How to set a cookie in Sinatra - ruby

I am developing a web application using Sinatra and Ruby. I need to set a cookie that is accessible from all subdomains. My original code was this:
#language = 'en-US'
cookies[:USER_LANGUAGE] = #language
This produced the desired effect (e.g. setting the cookie "USER_LANGUAGE" equal to "en-US"
However, it was not accessible from all subdomains. After looking at How to set a cookie on a separate domain in Rails and other similar questions, I have tried this:
#language = 'en-US'
cookies[:USER_LANGUAGE] = {
:value => #language,
:domain => '.example.com'
}
When I check the cookie data, it is set completely wrong. The value of the cookie is everything inside the brackets, and the domain is still only example.com (not .example.com)
Here is the value produced:
%7B%3Avalue%3D%3E%22en-US%22%2C+%3Adomain%3D%3E%22.example.com%22%7D

If you want all your cookies to be accessible from all subdomains, you can set the cookie options for your application:
set :cookie_options, :domain => '.example.com'
If just need it on one cookie, you can do this (instead of using the cookies object):
response.set_cookie(:USER_LANGUAGE, :value => #language, :domain => '.example.com')

Related

How to set SameSite to none in Rails 6 API with CreateReactApp?

I have a Rails 6 app acting as an API for a frontend app originally created with create-react-app. I am trying to store a session token to preserve login on page refresh.
When running locally, Chrome shows that SameSite=Lax but the session token is stored anyway. On the live site, SameSite is still Lax, but Chrome gives a little warning saying that the Set-Cookie header was blocked because it came from cross-site response. Both the frontend and api are deployed on Heroku in separate repos.
I've tried a number of things:
secure_headers gem, with the following in app/config/initializers/secure_headers.rb:
SecureHeaders::Configuration.default do |config|
config.cookies = {
secure: true, # mark all cookies as "Secure"
httponly: true, # mark all cookies as "HttpOnly"
samesite: {
lax: false
}
}
end
I have the following in app/config/initializers/session_store.rb:
Rails.application.config.session_store :cookie_store, :key => '_session_id',
:domain => :all,
:same_site => :none,
:secure => :true,
:tld_length => 3
I've tried cookie_serializer, although I commented everything out of that file.
I have the rack-cors gem, with the following in the cors.rb initializer:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:8080', 'http://localhost:5000', 'https://frontend-auth-frontend.herokuapp.com'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true,
exposedHeaders: ["Set-Cookie"]
end
end
I've tried a few other gems I saw recommended on SO but I don't remember all of them.
So... yea, how do I set SameSite so that my heroku frontend (react) can store cookies from the heroku api (rails 6)?
I can provide other info about the project, specific versions, etc., just not sure what else would be helpful.
Thanks!
the following code should set sameSite to none (the key is in the last line)
//config/application.rb
config.api_only = true
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore,
key: '_cookie_name', path: '/', same_site: :None, secure: true

Mailgun::CommunicationError via nginx '301 Moved Permanently' error

I have a Ruby web app that sends email via Mailgun.
My Mailgun account & gem are properly set up and I can send emails manually (via curl, for instance).
The API key and the API base URL (https sandbox domain) are stored in environment variables.
When I attempt to send emails from the app like this:
def initialize(mailer: nil)
#mailer = mailer || Mailgun::Client.new(ENV['MAILGUN_API_KEY'])
end
then:
def call(user)
mailer.send_message(ENV['MAILGUN_SANDBOX'], {from: '...',
to: user.email,
subject: '...',
text: "..."})
end
When I run the app with Sinatra via localhost:xxxx, I get a Mailgun::CommunicationError at /.../... 301 Moved Permanently: ... nginx pointing to this line:
mailer.send_message(ENV['MAILGUN_SANDBOX'], ...
Any idea why that happens? I've researched the issue for hours but couldn't find a clue on what to do next.
Thanks!
I ran into this same issue. If you have already fixed this then hopefully this can help someone else.
I switched over to message builder for ease of use and being able to render my html but I'm pretty sure it will still send with the format you have setup with :text
When I switched over to the proper domain in the .env file I believe it solved my issue. You'll need 2 different domains to use Mailgun. The first is the full domain for your sandbox. ENV['MAILGUN_DOMAIN'] it is the sandbox domain with the full https://api.mailgun.net/v3/sandboxXXXXxxxXXXXXX.mailgun.org to send most of the mail formats.
You'll also need the last half of the full domain to send messages. That's just the sandboxXXXXxxxXXXXXX.mailgun.org which is passed into the MessageBuilder or other message .send_message method. When I had them mixed up or both the same I kept on getting this error. When I switched over to separate the two in my development.rb and some_mailer.rb is when I could send the mail without a problem.
Below is my file setup, for reference. I'm pretty new to all of this but this is how I'm setup and it's working for me so hopefully it helps.
# .env
MAILGUN_DOMAIN='https://api.mailgun.net/v3/sandboxXXXXxxxXXXXXX.mailgun.org'
MAILGUN_SEND_DOMAIN='sandboxXXXXxxxXXXXXX.mailgun.org'
# development.rb
ActionMailer::Base.smtp_settings = {
:authentication => :plain,
:address => "smtp.mailgun.org",
:port => 587,
:domain => "ENV['MAILGUN_DOMAIN']",
:user_name => "ENV['MAILGUN_USERNAME']",
:password => "ENV['MAILGUN_PASSWORD']"
}
# some_mailer.rb
def some_mail_notification(user)
#user = user
mg_client = Mailgun::Client.new ENV['MAILGUN_KEY']
mb_obj = Mailgun::MessageBuilder.new
mb_obj.from "email#testing.com", {'first' => 'Customer', 'last' => 'Support'}
mb_obj.add_recipient :to, #user.email, { 'first' => #user.first_name, 'last' => #user.last_name }
mb_obj.subject "Your Recent Purchase on Some Site"
mb_obj.body_html ("#{render 'some_mail_notification.html.erb'}")
mg_client.send_message("sandboxXXXXxxxXXXXXX.mailgun.org", mb_obj)
end
I left the send_message above to the sandbox domain but you can set that as an environment variable in the .env file.

How to use ruby-aws-skd#url_for with custom domain s3 bucket?

I have a working bucket with a subdomain assigned on s3, and I want to create a signed url for a resource using the url_for method of an object with the expire option.
On a initializer, I have:
AWS.config({
:access_key_id => Rails.application.secrets.aws_access_key_id,
:secret_access_key => Rails.application.secrets.aws_secret_access_key,
:s3_endpoint => "assets.mydomain.com"
})
When I run
AWS::S3.new.buckets["assets.mydomain.com"].objects["image.jpg"].url_for(:get, { :expires => 20.hours.from_now }).to_s
I get
"https://assets.mydomain.com/assets.mydomain.com/image.jpg?WSAccessKeyId=xxx&Expires=xxx&Signature=xxx"
Note the bucket url twice.
What do I have to do so this works properly?
PS: Removing the endpoint and using the normal s3 url (http://s3-west-1.amazon...) works as expected, but I want to use my custom dns entry.
Thanks.
Try using the option force_path_style: false to use the subdomain style.
AWS::S3.new.buckets["assets.mydomain.com"].objects["image.jpg"]
.url_for(:get, {:expires => 20.hours.from_now, force_path_style: false }).to_s
From http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/S3Object.html#url_for-instance_method

How to decode a cookie from the header of a websocket connection handshake? (Ruby)

I am running a Sinatra app within an EventMachine.run loop and in my ws.onopen method I wish to check the handshake header's cookie to ensure that the incoming request is coming from a registered user of my webapp.
My Sinatra app includes the following:
use Rack::Session::Cookie, :key => COOKIE_KEY,
:path => '/',
:expire_after => 2592000, #30 days
:secret => COOKIE_SECRET
and my ws.onopen method looks like this (trimmed)
ws.onopen { |handshake|
cookie, bakesale = handshake.headers['Cookie'].split('=')
rack_cookie = Rack::Session::Cookie.new(MyApp, {
:key => COOKIE_KEY,
:path => '/',
:expire_after => 2592000, #30 days
:secret => COOKIE_SECRET
})
decoded = rack_cookie.coder.decode(bakesale)
puts "decoded: #{decoded}"
}
The value of cookie matches my COOKIE_KEY just fine, however the value of decoded is nil
How should I decode the incoming cookie data?
-- some time later --
I've changed the above slightly to
ws.onopen { |handshake|
cookie, bakesale = handshake.headers['Cookie'].split('=')
rack_cookie = Rack::Session::Cookie.new(MyApp, {
:key => COOKIE_KEY,
:path => '/',
:expire_after => 2592000, #30 days
:secret => COOKIE_SECRET,
:coder => Rack::Session::Cookie::Base64.new
})
puts rack_cookie.coder.decode(bakesale)
}
and that outputs
?q?[?????ov??????to?Z???294cb6e2b95e9?##v3???#c&F3#SC?CSC#CSs?c3sSCCs?cCm;FI"__FLASH__;F{I" user;FU:Moped::BSO?㣤?&?V7D?B!
which looks like it needs marshalling.
However Marshal.load (rack_cookie.coder.decode(bakesale)) throws an exception, saying dump format error for symbol(0x10)
-- and even more time later --
I also tried rack_cookie.coder.decode(bakesale.split('--').first)
which resulted in
??H?d????=?d:ETI"E7ce599b294cb6e2b95e9?##v3???#c&F3#SC?CSC#CSs?c3sSCCs?cCm;FI"__FLASH__;F{I" user;FU:Moped::BSO?㣤?&?V7D?B!
So as you can see, there is a minor difference, but either way I need to somehow turn that into a valid hash.
Marshal.load(rack_cookie.coder.decode(bakesale.split('--').first)) still results in dump format error for symbol(0x10) either way.
So I feel I'm closer, but no cigar as yet.
The answer is to use Rack::Utils.unencode.
I now have this working
Marshal.load(rack_cookie.coder.decode(Rack::Utils.unescape(bakesale.split('--').first))) decodes perfectly to the hash I need, allowing me to extract the user ID. W00t!
Many thanks to User spastorino over at https://github.com/rack/rack/issues/551 for pointing me in the right direction.

Grape API and HTTP Digest Authentication

I am working on creating an API for my ruby application that authenticates users based on HTTP Digest Authentication. I decided to use the Grape API library because it makes creating an API cleaner in ruby. The Grape documentation states that you can use Digest Authentication like:
http_digest({ :realm => 'Test Api', :opaque => 'app secret' }) do |username|
# lookup the user's password here
{ 'user1' => 'password1' }[username]
end
The Grape implementation above is a wrapper for Rack::Auth::Digest::MD5
Now also for security i read that as of RFC 2617 you don't need to store the password as plain text in the database you store an MD5 digest of the username:realm:password and authticate against that so i created a DataMapper model:
class Key
include DataMapper::Resource
property :id, Serial
property :username, String
property :password, String
property :active, Boolean, :default => true
property :created_at, DateTime, :default => DateTime.now
property :updated_at, DateTime
end
Now with what I provided, I am lost as to how to connect these two and make it work.
Unfortunately, Rack::Auth::Digest::MD5 expects a plaintext password on the server side.
The Grape example code shows a hard-coded lookup of password.
You could replace { 'user1' => 'password1' }[username] with
Key.first( :username => username ).password
provided you stored plaintext passwords in the Key class. You could store these reversibly-encrypted I suppose, although that doesn't add much security unless you construct relatively complex/costly schemes for key management.
Not sure if there is a way around this that would let you store hashed passwords. MD5 isn't the most secure hashing choice (although better than nothing!). If security is an important concern for your API, you will want to look beyond digest auth - using https would help, for example.
Edit: Following a bit of to-and-fro in discussions, the following variation of Grape's example does allow you to store the MD5'd password:
auth :http_digest, { :realm => { :realm => 'Llama', :passwords_hashed => true, :opaque => "7302c32d39bbacb5ed0ace096723fd" } } do |username|
Digest::MD5.hexdigest( 'fred:Llama:654321' )
end
The example gives a hard-coded username:'fred', password:'654321' response. So I think your target code is something like:
auth :http_digest, { :realm => { :realm => 'Llama', :passwords_hashed => true, :opaque => "7302c32d39bbacb5ed0ace096723fd" } } do |username|
k = Key.first( :username => username )
k ? k.password : nil
end
And you store the result of Digest::MD5.hexdigest( "#{username}:#{realm}:#{password}" ) in each user's password property.
Note the double-level hash with :realm twice. This is a bit hacky, but at least you don't have to write your own middleware, Grape is still dealing with it. This is not a documented feature of Grape or covered with tests, so may not work in future versions.

Resources