Difference in cookie content using Sinatra session & Rack::Session::EncryptedCookie - ruby

I'm learning Sinatra framework & developing a login system. I came across two ways of using cookies.
A simple Sinatra inbuilt way:
enable :sessions
set :session_secret, 'random-key'
This approach produces following cookie content while logged in (used session.inspect to get the output):
{"session_id"=>"6be0b9a31831604ba51114d265ba952482e0b2da6ced6c54e15ebe7f212858ca",
"tracking"=>{"HTTP_USER_AGENT"=>"b8c1e8f89eeaea0b825bed0d811f0c7678e98c74",
"HTTP_ACCEPT_ENCODING"=>"a0bfc876d68fe7aea700da5ea8925abac6f2f794",
"HTTP_ACCEPT_LANGUAGE"=>"dd065ed263c67d799f943ab6c39b55c5e008cbb5"},
"csrf"=>"b480324f510e4f391d15cee8236a8fb74a5aaa5ce2f9ad38e4dbb025a823b16e",
"name"=>"john"}
Another approach is using an encrypted cookie :
require 'sinatra'
require 'encrypted_cookie'
use Rack::Session::EncryptedCookie, :secret => "random-key"
But this approach produces following cookie content while logged in (used session.inspect here too):
{:name=>"john"}
Why enable :sessions is creating such a big cookie with all that information & why is it required (especially those HTTP_... parts?) Because Rack::Session::EncryptedCookie isn't generating any of it.
Do you think that using enable :sessions should be preferred because it has csrf token & session id? Or do you think that Rack::Session::EncryptedCookie is enough since it is encrypted?
I have following versions of gems installed :
encrypted_cookie (0.0.4)
rack (1.5.2)
rack_csrf (2.4.0)
sinatra (1.4.3)
thin (1.5.1)
Please tell me if you need more information...

Because Sinatra will use rack-protection middleware when you enable :sessions. It makes cookie bigger but more secure.
Relevant snippet:
def setup_default_middleware(builder)
builder.use ExtendedRack
builder.use ShowExceptions if show_exceptions?
builder.use Rack::MethodOverride if method_override?
builder.use Rack::Head
setup_logging builder
setup_sessions builder
setup_protection builder
end
def setup_sessions(builder)
return unless sessions?
options = {}
options[:secret] = session_secret if session_secret?
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
builder.use session_store, options
end
def setup_protection(builder)
return unless protection?
options = Hash === protection ? protection.dup : {}
options = {
img_src: "'self' data:",
font_src: "'self'"
}.merge options
protect_session = options.fetch(:session) { sessions? }
options[:without_session] = !protect_session
options[:reaction] ||= :drop_session
builder.use Rack::Protection, options
end
sessions? returns true if you enable :sessions
session_store is Rack::Session::Cookie by default
The difference between Rack::Session::EncryptedCookie
That is, if you want to use Rack::Session::EncryptedCookie with rack-production, it can be easily done by:
enable :sessions
set :session_store, Rack::Session::EncryptedCookie
FYI, since encrypted_cookie is lack of some features (secret rotation, custom serializer, etc) and no longer under maintenance, I made another one to replace it.
Hope it helps.

Because Rack::Session::EncryptedCookie requires that your secret be at least 16 bits long. In the README, they recommend using OpenSSL for generating the secret, like so:
ruby -ropenssl -e "puts OpenSSL::Random.random_bytes(16).inspect"
If you open your inspector, you'll see a cookie named 'rack.session', and its contents obfuscated.

As I know, when using Rack::Session::Cookie in Sinatra, and write session_secret as an environment variable, the session which has created won't destroy after the project deploy. I think this is a risk in Single Page Application.

Related

Passing headers (or other settings) from Sinatra through Puma

I am trying to learn how to develop a simple web app using pure Ruby. I have a simple backend service which I created as a Sinatra app. While developing it, I noticed the frontend (a simple HTML/JS static site) would not communicate with it because of CORS policies. So I looked into how to pass headers from Sinatra.
I came across sinatra-cors. I set it up as instructed and my app looks like this (abbreviated):
require 'sinatra'
require 'sinatra/cors'
set :allow_origin, '*'
set :allow_methods, 'GET,HEAD,POST'
set :allow_headers, 'content-type,if-modified-since,access-control-allow-methods,access-control-allow-origin'
set :expose_headers, 'content-disposition'
set :allow_credentials, true
post '/' do
[...]
end
When I run it with ruby app.rb, it works perfectly. Frontend can communicate and CORS policies are observed.
Now, I want to set up the service for a production environment. For that, I want to use Puma. So with Puma, I have a config.ru which looks like this:
require File.expand_path('app', File.dirname(__FILE__))
run WebApp
and I modified my app.rb to look like this (again abbreviated):
require 'sinatra'
require 'sinatra/cors'
class WebApp < Sinatra::Application
set :allow_origin, '*'
set :allow_methods, 'GET,HEAD,POST'
set :allow_headers, 'content-type,if-modified-since,access-control-allow-methods,access-control-allow-origin'
set :expose_headers, 'content-disposition'
set :allow_credentials, true
post '/' do
[...]
end
end
basically, wrapped the app in a class, and call it from the config.ru. When I run this by running puma in the directory, the service comes up, but headers are no longer passed back. Whenever I try to hit the backend, I get:
Access to XMLHttpRequest at 'http://localhost:4567/' from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
This was the error I was getting before I originally set up the headers. So it seems pretty clear to me that the set parameters are being ignored.
So, this seems like a simple matter, but I have not been able to find a proper answer: How do I make Puma respect the 'set' parameters? Or alternatively, how do I achieve the same desired result?
It seems clear to me that I am missing a very simple thing, but I cannot figure out what exactly it is.
Thanks in advance!
It looks like you are just missing register Sinatra::Cors in your class.
class WebApp < Sinatra::Application
register Sinatra::Cors # Add this line.
set :allow_origin, '*'
# etc.

Sinatra Session Caching

I'm having a little trouble understanding how to get Sinatra to cache sessions. This problem manifests itself when I fire up more than one application instance (ala Puma or multiple Thins).
I'm asking Sinatra (1.4) to use sessions like this:
enable :sessions
set :session_secret, 'secret sauce'
set :protection, except: :session_hijacking
$connections = []
set connections: $connections
I've left the $connections in there to demonstrate that this App is using server sent events, in case that has any relevance.
I am them using Persona (Mozilla) to support logins/authentication and am ultimately storing the logged in email in Sinatra's session with:
session[:auth_email] = data["email"]
Where data is given to me by Persona.
What I can't work out is how I ask Sinatra to store session data in a persistent store (ideally Redis) so the app can be recycled without losing session state (I've done this with Ramaze before with Ramaze::Cache.options.session = Ramaze::Cache::Redis.using())
Check out Moneta.
The project's Github page gives examples of how to set this up and is extremely easy to integrate with Sinatra.

Sinatra + Rack::Session::Pool + Moneta

I'm using server side session handling with Moneta in my Sinatra application.
The part of my config.ru looks like that:
require 'rack/session/moneta'
use Rack::Session::Moneta do
use :Expires
adapter :Memory
end
How long does is take for sessions to expire? I couldn't find documentation for it.
I currently delete sessions with
get '/logout'
session.destroy
end
But I believe that only destroys the session cookie on the client side.
How can I find the sessions which are currently active?
There is a variable called #pool in Rack::Session. How can I access it from my Sinatra app?
You would set the expiration time when storing/accessing session keys by adding the expires: n option (set n to 0/false to have disable expiration). Here is the relevant entry in the Moneta README.

Sinatra/1.4.3 use Rack::Session::Cookie warning

my configuration code
require 'sinatra'
#set :environment, :production
enable :sessions
enable :logging
set run: true
case
when production?
set port: 8081
when development?
require 'sinatra/reloader'
require 'better_errors'
use BetterErrors::Middleware
BetterErrors.application_root = __dir__
end
use Rack::Session::Cookie, key: 'N&wedhSDF',
domain: "localhost",
path: '/',
expire_after: 14400,
secret: '*&(^B234'
get '/' do
erb :hello
end
It still shows a warning:
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
This poses a security threat. It is strongly recommended that you
provide a secret to prevent exploits that may be possible from crafted
cookies. This will not be supported in future versions of Rack, and
future versions will even invalidate your existing user cookies.
but it doesn't show up on production
the question is, why does it still show the warning even if the Rack::Session::Cookie already set?
You are using both
enable :sessions
which makes Sinatra setup cookie based sessions, and
use Rack::Session::Cookie, ...
which also adds sessions to your app, so you end up with two instances of Rack::Session::Cookie in your middleware stack.
The warning is being generated by the session middleware included by Sinatra. By default Sinatra doesn’t create a session secret when running in the development environment (in classic mode at least, it does for modular apps), and so Rack generates the warning in development.
You should only need one of the two ways of enabling sessions, using two together could result in them interacting in unexpected ways.
To avoid the warning, you can explicitly set a secret for the Sinatra session with the session_secret option:
enable :sessions
set :session_secret, '*&(^B234'
You can also pass the options hash as an argument when enabling sessions. Instead of enable :sessions, do this:
set :sessions, key: 'N&wedhSDF',
domain: "localhost",
path: '/',
expire_after: 14400,
secret: '*&(^B234'

Devise with Sinatra

Does anyone had installed Devise gem with Sinatra?
Devise is based on Warden and so it should work on Sinatra, I couldn't find any related info about how to implement it.
Devise is really just a Rails-centric wrapper with nice helpers for warden, which is the underlying Rack authentication framework. So if you're using Sinatra in conjunction with Rails, you can use Devise in your Rails app, and use warden directly in your Sinatra app, and they will see the same user session data.
So no, you can't use Devise directly within your Sinatra app, but if you're building a modular app with some pieces in Rails, and other pieces in Sinatra, you can use Devise/Warden among the components.
Devise is designed for Rails only. You can't use it with Sinatra.
You can check out:
https://github.com/maxjustus/sinatra-authentication
http://www.gittr.com/index.php/archive/sinatra-basic-authentication-selectively-applied/
https://gist.github.com/243611
There is also https://github.com/jsmestad/sinatra_warden available.
I was able to get it working. There were a few main aspects:
Get Devise working with Rails (Devise is a Rails app, won't work without it)
Setup the mapping (route) on Rack level to support both Rails and Sinatra
Share the sessions between Rails and Sinatra
Setup Warden and make it available to Sinatra
Here is most relevant part of code from /config.ru:
#
# ...
# Rest with Rails
map "/" do
run MyApp::Application
end
# Anything urls starting with /slim will go to Sinatra
map "/slim" do
# make sure :key and :secret be in-sync with initializers/secret_store.rb initializers/secret_token.rb
use Rack::Session::Cookie, :key => '<< see, initializers/secret_store.rb >>', :secret => '<< copy from initializers/secret_token.rb >>'
# Point Warden to the Sinatra App
use Warden::Manager do |manager|
manager.failure_app = AppMain
manager.default_scope = Devise.default_scope
end
# Borrowed from https://gist.github.com/217362
Warden::Manager.before_failure do |env, opts|
env['REQUEST_METHOD'] = "POST"
end
run AppMain
end
See, http://labnote.beedesk.com/sinatra-warden-rails-devise for a complete solution.

Resources