Sessions in sinatra app being shared across browsers - ruby

I'm using Sinatra framework to make a very simple web app that requires a session for login.
I can login just fine, but when my friends visit the site they are logged in as me. I had a friend sign-up. When I re-visited the site I was logged in as her!! We're 3000 miles apart on different devices.
Here is the gist of my login code..
require 'rubygems'
require 'sinatra'
# I have tried enabling/disabling the :session_secret
# set :session_secret, 'my_secret'
enable :sessions
before '*' do
begin
User.login(User.find(session[:me])) if session[:me]
end
end
post '/login' do
user = User.find_by_email!(params[:email]).authenticate!(params[:password])
session[:me] = user.id
User.login user
200
end
I also tried adding this to my rackup file
use Rack::Session::Cookie,
:key => 'my_app_key',
:path => '/',
:expire_after => 14400, # seconds
:secret => 'secret_stuff'
The issue is both on production and development environments.
Sinatra documentation does not recommend any config beyond enable :sessions
http://www.sinatrarb.com/faq.html#sessions

User.login(User.find(session[:me])) if session[:me]
class User
def self.login(user)
##me = user
end
end
Here's your problem. Class variables persist, even between requests.
You login
User ##me gets set to your user
Someone elses visits the site.
session[:me] is nil since there is no session yet
User.login is not called, which would either set User ##me to a user or to nil.
So when there is no session[:me], User ##me doesn't get changed.
In short, do not use class variables to store information that should only persist for a single request.

Related

Sinatra not persisting session with redirect on Chrome

Sinatra is not persisting my session with a redirect on Chrome. It is creating an entirely new session and i'm losing all my previous session data.
As an example (similar to the Sinatra docs), i'm doing something like this:
enable :sessions
get '/foo' do
session[:user_id] = 123
session[:session_id] # "ABC", for example
redirect to('/bar')
end
get '/bar' do
# this is "DEF" when responding to Chrome (wrong),
# but "ABC" when responding to Firefox or Safari (right)
session[:session_id]
# this is nil when responding to Chrome (wrong),
# but 123 when responding to Firefox or Safari (right)
session[:user_id]
end
I'm thinking this has something to do with how the different browsers respond to handling the session after a redirect response. Has anyone seen something similar to this, or have any ideas on how to resolve this while still using sessions?
Thanks in advance!
Add this to your main app file:
use Rack::Session::Cookie, :key => 'rack.session',
:path => '/',
:secret => 'some-random-string'
With that added, you should be able to assign session['whatever'] and have it work as expected.
By doing enable :sessions you just get access to session per request.
Sinatra has no way to keep the reference to the previous call (your redirect) as it is treated as another request.
Thus, long story short:
set :session_secret, "SecureRandom.new(10) generated thing"
enable :sessions
always use enable :sessions with a secret, otherwise your session is recreated every time rack sees a request.
Please try to disable all custom cookie managament extensions is Chrome if any.
After that check headers in Developer tools → Network. Should see 'Cookie:' field.
I think that just because you didn't set :session_secret, refer to my answer on here

How do I use Devise to secure the delayed_job_web interface?

I'm using the delayed_job_web gem to monitor delayed jobs. https://github.com/ejschmitt/delayed_job_web
It is accessible using this line in my routes.rb:
match "/delayed_job" => DelayedJobWeb, :anchor => false
Every other area of my site requires a login using the Devise gem. How do I make this require a login too?
In the readme, they suggest adding the following to the config.rb:
if Rails.env.production?
DelayedJobWeb.use Rack::Auth::Basic do |username, password|
username == 'username'
password == 'password'
end
end
But that just uses plain text browser authentication.
UPDATE:
I tried something similar to the railscast on resque, and I think it's on the verge of working but giving me a redirect loop now:
authenticate :admin do
mount DelayedJobWeb, :at => "/delayed_job"
end
Any thoughts on why would it be giving a redirect loop?
Thanks,
Use authenticated instead of authenticate as described here: http://excid3.com/blog/rails-tip-5-authenticated-root-and-dashboard-routes-with-devise/
Works for me!
You could do something like this define this inside config/routes.rb file
authenticate_user = lambda do |request|
request.env['warden'].authenticate?
end
constraints authenticate_user do
mount DelayedJobWeb, :at => "/delayed_job"
end
Alternately if you have cancan for any other role management library you could do it something like this
I have used both of this in my applications to control access to resque-web depending on the needs of the application
Hope this help
Now it's 2017 I tried the other solutions and they didn't work, but the following admin check does work:
authenticated :user, -> user { user.admin? } do
mount DelayedJobWeb, at: "/delayed_job"
end

Sinatra session members "disappearing"

I've successfully troubleshooted an issue with session members not being available even though they were set and would like to know why it's happening. My situation can be described as:
Sinatra app using :session.
Using oAuth to authorise users and in the process setting a :ret_url session member so that the app knows where to come back to after auth.
Server is unicorn on Cedar stack (Heroku)
This works perfectly whilst running locally but the :ret_url session member was completely disappearing from the session on Heroku. I found that if I removed this code it fixed the problem:
before do
cache_control :public, :must_revalidate, :max_age => 60
end
Question 1: I'm guessing that my cookie was being cached without the :ret_url value and that's why it was breaking?
Question 2: I was setting the session member as shown in the route condition code below, is this the wrong place to do it?
# redirect users to login if necessary
set(:auth) do |access_token|
condition do
if request.request_method == 'GET'
session[:ret_url] = request.path_info
end
redirect '/' unless user_logged_in?
end
end
I'd like to use cacheing and still have my cookie be valid.
Hard to see what is going on without knowing all details, but there is a simple rule that you are most probably violating: do not use http caching on actions that are supposed to do something (other than just show page). When http caching is on, you browser does not even try to re-load the page and renders it from browser cache.
Cookies are not cached anywhere, the only thing cache_control does is setting CacheControl http response value
In your case the best thing you can do is to add list of routes that have no-action pages to your before block:
before '/my/static/page' do
cache_control :public, :must_revalidate, :max_age => 60
end
Most probably you will have very limited set of routes where you can benefit from http caching
A chap by the name of Ari Brown (waves at Ari), who is not a member here but deserves the credit for this answer, pointed me at the right solution, which is, as per the Sinatra FAQ, to not use enable :sessions but to use Rack::Session::Cookie as per
use Rack::Session::Cookie, :key => 'rack.session',
:domain => 'foo.com',
:path => '/',
:expire_after => 2592000, # In seconds
:secret => 'change_me'
I've added this into my config.ru and all is well.
I also noticed over in this post the alternative suggestion to set :session_secret, 'change_me' and, indeed, to do this via an environment variable, namely:
$ heroku config:add SESSION_KEY=a_longish_secret_key
then in your app
enable :sessions
set :session_secret, ENV['SESSION_KEY'] || 'change_me'
Obviously you can use the environment variable strategy with the Rack::Session::Cookie approach too. That's the way I went as it offers more flexibility in configuration.
The reason these work is that the cache controller middleware is farming requests out to multiple server instances and without setting a session secret it's just making one up per server, and thus breaking the sessions.

Rack Session Cookie and Sinatra - setting and accessing data

I was using Rack Session Pool, however my users would get kicked off one webserver thread onto another making the session data expire. I started toying around with just enable :sessions in Sinatra, however I am unable to use that because I have mutliple apps using Sinatra (same key it appears to be using - not sure if this is because its the same host or not)
So since my apps would break each other, I now am trying Rack Session Cookie and setting the variables (same thing as enable :sessions, but you can set the variables)
Great so that works! But now I cannot access the session data the way I was using it, in Rack Session Pool and in enable: sessions
session[:user] = nick
puts session[:user]
you get the idea...
Question is why can I access session data with session[:user] in Pool and Sinatra enable :sessions, but not in Rack Session Cookie? Am I missing anything? All I am doing is below
config.ru
use Rack::Session::Cookie, :key => 'key',
:domain => "localhost",
:path => '/',
:expire_after => 14400, # In seconds
:secret => 'secret'
EDIT:
Did some more testing and found that it's actually putting it in the session variable, however as soon as it moves to a new method or redirection the session variable appears to be dropped (is this cookie really larger than 4KBs?!) - it can't be because enable :sessions works just fine
Here's what I did to fix this problem:
use Rack::Session::Cookie, :key => 'my_app_key',
:path => '/',
:expire_after => 14400, # In seconds
:secret => 'secret_stuff'
Do you see the difference from the above? - No Domain, if I let Rack::Session::Cookie specify the domain or the browser (whoever does it), I have no errors between mutliple Sinatra/Rack apps...
Problem is with the domain 'localhost'. This thread describes in more details as to why having localhost as the domain wouldn't work: Cookies on localhost with explicit domain
A fix would be to setup a domain in your hosts file like
127.0.0.1 superduper.dev
Then set your domain in your sessions settings to superduper.dev. Then during development you can go to whatever port you might need. Ex. superduper.dev:5000

Sinatra: How do I provide access to a login form while preventing access to the rest of my Sinatra app?

I recently created a Sinatra app with a login form (no basic auth). To prevent access to the app unless the user logged in I put a before block in place
before do
unless request.path_info == '/login'
authenticated?
end
end
I quickly realized that this prevented me from accessing resources in the public directory like my style sheet and logo unless authenticated first as well. To get around that I changed my filter to the following:
before do
unless request.path_info == '/login' || request.path_info == "/stylesheets/master.css" || request.path_info == "/images/logo.png"
authenticated?
end
end
If there were lots of resources I needed to provide exceptions to this way of making them would quickly become overwhelming. What is a better way to code this so I can make exceptions for the public directory or even its specific sub-directories and files like /stylesheets, /images, /images/bg.png but not /secret or /secret/eyes-only.pdf?
Or ... Is there a completely different best-practice to handle this situation of locking down everything except the stuff related to logging in (handlers, views, resources)?
You could extract the login logic into it's own Rack middleware (which can be a Sinatra app).
The authentication middleware will serve the public files.
require 'sinatra'
class Authentication < Sinatra::Base
def logged_in?
# your login logic goes here
end
get '/login' do
# login formular and logic here
end
get(//) do
pass if logged_in?
redirect '/login'
end
end
configure { |c| c.use Authenitcation }
get('/') { ... }
Instead of putting the authorization information into your Sinatra application directly, why don't you extract it into Rack using Rack::Auth:
# my_app.ru
app = Rack::Builder.new do
use Rack::Static, :urls => /^(stylesheets|javascripts|images|fonts)\//
map '/login' do
run MyApplication
end
map '/' do
use Rack::Auth::Basic do |username, password|
# check the username and password sent via HTTP Basic auth
end
run MyApplication
end
end

Resources