Update user newsletter subscriptions without being logged - ruby-on-rails

I have a User model with authentication.
If logged in, the user can update his newsletter subscriptions preferences (i.e. option/optout)
However, I'd also like to give limited access to the page for the user who hasn't confirmed his account yet (through a link in an email).
He wouldn't be able to change any other information but the subscription preferences.
Right now, I can only unsubscribe a user from one newsletter.
What I'd like is to have a form with all the subscriptions available.
What would be the process to do so to make it secure?
(i.e. the user wouldn't be able to change the email/username to update another user from the link provided)

You can generate a random SHA1 or MD5 hash using Ruby's Digest module and inject it to the link you send out to the user.
Make sure to check the hash matches the user's email, a rough algorithm is as follows:
Create a hash for the user in question and store it in the database:
user.email_hash = Digest::SHA1.hexdigest(user.email)
user.save
Within the controller method handling the email verification, check if the hash matches the user's details
if current_user.email_hash == params[:email_hash]
# A match, process the activation and invalidate the hash
user.activate
user.invalidate_hash # This can set the email_hast to nil, for example.
else
# Someone's trying to be sneaky, or accessing an invalidated hash
end
There are ways to make this more secure, for example having a combination of a user's username and email to create the hash, putting the endpoint behind a rate limit to prevent brute force attempts at guessing hashes, etc. but this should provide a good starting point.
Finally, don't forget to require the digest\sha1 module (require 'digest/sha1') in any files that need it.

A pattern that many newsletters use is to generate long random tokens and inject them into the link URLs provided in the e-mail. Each random token maps to an user; that way, by checking the token parameter, it is possible to get the user associated to that e-mail.
As an example, suppose the newsletter page is /newsletters/settings. The actual link sent by e-mail is /newsletters/settings?code=12345678901234567890, where the code parameter is different per user, or even per user and sent e-mail in some cases.
These tokens have security implications, though. For instance, if a newsletter is forwarded to another person because they forward the original e-mail. If they don't remove the links, third parties could impersonate the original user and manage the subscription as well.

Related

How do I update a user without a token while using Devise token Auth?

As a part of my application, when I send an invitation out to other users, I need to set a bunch of parameters for the newly created user before they even sign into the application. So, before switching to DeviseTokenAuth(and rails api), I used devise and it allowed me to make user changes from the back-end. For example, if I go to the console and do a user.save, it returns true.
Now after switching to DeviseTokenAuth, I had to enter the following in concerns file within the User model:
include DeviseTokenAuth::Concerns::User
Once i included this, I am unable to make changes to the user from the backend, (for example, rails console> user.save returns false). I presume that it requires a Token every time a user is updated? How can I skip this for specific controller actions where for instance, I would need to update the user withouth the user/client actually calling the action. (sending a token)
In case somebody else comes across this issue: I spent a few hours shooting in the dark finally to realize that it that the uid field for the User object needs to be populated with DeviseTokenAuth. If not, user will not be updatable. i.e.user.save will return false
In my case, I was sending an invitation using devise_invitable which is currently outside the bounds of DeviseTokenAuth defaults. So, when you create a new user using the invite method of Devise_invitable gem, the UID, provider fields are not automatically set. I just needed to add 2 line of code post invitation:
u= User.invite!(params[:email])
u.uid=params[:email] #added
u.provider='email' #added
u.save #added
Now the user object works fine, it had nothing to do with the token itself - so my previous assumption stated in the question was humbug.

Getting past anti-CSRF to log a user into a site when you know their username and password

This sounds a bit evil, bear with me though. It's also not specifically a Rails question even though the two sites in question use Rails. (Apologies in advance for both these things)
Imagine two websites which both use Ruby on Rails:
mysite.com, on which i'm a developer and have full access in terms of changing code etc, and also have an admin login, so I can manage user accounts.
theirsite.com, on which i have an admin login but no dev access. I know the people who run it but i'd rather not ask them any favours for political reasons. That is an option however.
Using my admin login on each site i've made a user account for the same person. When they're logged into mysite.com, i'd like to be able to provide a button which logs them straight into theirsite.com. I have their username and password for theirsite.com stored in their user record in the mysite.com database, to facilitate this. The button is the submit button for a form which duplicates the form on the theirsite.com login page, with hidden fields for their username and password.
The stumbling block is that theirsite.com handles CSRF with an authenticity_token variable, which is failing validation when the login submits from mysite.com.
My first attempt to get past this was, in the mysite.com controller which loads the page with the form, to scrape the theirsite.com login page to get an authenticity token, and then plug that into my form. But this isn't working.
If i load the theirsite.com login page, and the mysite.com page with the remote login button in two browser tabs, and manually copy the authenticity_token from the theirsite.com form to the mysite.com form, then it works. This is because (i think) the authenticity_token is linked to my session via a cookie, and when i do it all in the same browser the session matches up, but when i get the authenticity token from theirsite.com via scraping (using Nokogiri but i could use curl instead) it's not the same session.
Question A) So, i think that i also need to set a cookie so that the session matches up between the browser and the Nokogiri request that i make. But, this might be impossible, and exactly the sort of thing that the anti-CSRF system was designed to defeat. Is that the case?
Question B) Let's say that i decide that, despite the politics, i need to ask the owner of theirsite.com to make a small change to allow me to log our users into theirsite.com when we know their theirsite.com username and password. What would be the smallest, safest change that i could ask them to make to allow this?
Please feel free to say "Get off SO you evil blackhat", i think that's a valid response. The question is a bit dodgy.
A) No, this is not possible as CSRF Protection is made to protect from actions like these only. So "Get off SO you evil blackhat"
As per the question I'm assuming that theirsite.com is using Rails(v3 or v4)
B) The smallest change that you could ask them to do is to make a special action for you, so that you could pass user credentials from your back-end and the user will be logged in from their on.
That action will work something like this :
You'll have a special code which will be passed along the credentials so that the request is verified on their servers. That code can either be a static predefined code or it can be generated on minute/hour/day basis with the same algorithm on both sites.
The function that you'd be asking to make for you will be like this:
Rails v3 and v4:
This action will be POST only.
#I'm supposing 'protect_from_forgery' is already done in theirsite.com
class ApplicationController < ActionController::Base
protect_from_forgery
end
#changes to be made are here as follows
class SomeController < ApplicationController
skip_before_filter :verify_authenticity_token, only: [:login_outside] #this turns off CSRF protection on specific actions
def login_outside
if(#check special code here)
#Their login logic here
end
end
end
Check this link for further information on skipping CSRF protection in Rails
Rails 4 RequestForgeryProtection
This shouldn't be too hard to do.
You need to send an ajax GET request to their signup page, copy the authenticity_token with javascript, and then send an ajax POST to the actual log in route that creates a session with the right credentials and authenticity_token.
One tricky part is finding out their log in route. Try /sessions/new or perhaps they have the url in the form, so look at the html there. Good luck!
The other tricky part is knowing how the parameters are usually sent. Check out the form's html. If all the input tags have user_ before their name's then you'll need to structure your parameters similarly; i.e. user_email, user_password.
It's entirely possible to fetch the crsf token and submit your own form (because a log-in page is accessible to anyone!). However, it'll be difficult to know the details of their arrangement. The guessing and checking isn't too bad of an options (again, /sessions/new is how I route my log in; you should also try your route to see if they have a similar one.)
If that doesn't work, try taking a look at their github account! It's very possible they haven't paid $7 a month and it's open to the public. You will easily be able to view their routes and parameter parsings that way.
Good luck!
This is impossible. The anti-csrf works like you send cookie to an user, and inject token in form of hidden field into a form; if the token matches with cookie form post is accepted. Now if you run form on your side, you can't set the cookie (as the cookie can be only set in domain of its origin).
If there is just some particular action you want to perform on their site, you can get away with browser automation. (i.e. your run browser on your server-side, script the action and execute it).
As for B) safest and smallest change is contradiction :) Smallest change would be to create handler for POST request on their side where you'll send username and password (this handler HAS TO run over https) and it will create auth cookie on their side.
As for safest - the whole concept of storing encrypted (not hashed) passwords is questionable at best (would you like your site to be listed here http://plaintextoffenders.com/ ?). Also if user changes his password on their side you're screwed. Secure solution would be that you'll store just 3pty UserID on your side, and you'll send asymmetrically encrypted UserID with Timestamp to their side (you'll encrypt it with your private key). They'll decrypt it (they'll have to have public key), validate if timestamp is not to old and if not they'll create auth cookie for given user id. There are also protocols for that (like SAML).
A)
What you are trying to do is really a form of a CSRF attack.
The idea behind a cross-site request forgery attack is that an attacker tricks a browser into performing an action as a user on some site, as the user who is using the site. The user is usually identified by a session identifier stored in a cookie, and cookies are sent along automatically. This means that without protection, an attacker would be able to perform actions on the target site.
To prevent CSRF, a site typically includes an anti-CSRF token in pages, which is tied to the session and is sent along in requests made from the legitimate site.
This works because the token is unpredictable, and the attacker cannot read the token value from the legitimate site's pages.
I could list various ways in which CSRF protection may be bypassed, but these all depend on on an incorrect implementation of the anti-CSRF mechanism. If you manage to do so, you have found a security vulnerability in theirsite.com.
For more background information about CSRF, see https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF).
B)
The smallest change which theirsite.com could do is to disable the CSRF protection check for the login page.
CSRF protection depends on the unpredictability of requests, and for login pages, the secret password itself protects against CSRF. An extra check through an anti-CSRF token is unnecessary.

Rails & Devise - Autologin Across Subdomains

Setup
I have a Rails application where users register for an account, and a subdomain is created for them. They can then proceed to the subdomain and log in with their credentials. The workflow looks something like this:
User visits base domain fills out a form that with email/username/password and subdomain fields
From the submitted info, the server creates an account in the global/public database. Server then creates a database that will be specific to that particular subdomain/account, and stores the user record in it.
User is redirected to their subdomain, and asked to log in.
(note: to implement the separate "databases", I'm using postgres schemas, but that should be irrelevant.)
The question
My question involves step 3. I would like to redirect the user to their subdomain and log them in automatically instead of asking them to log in. However, I do not want to share a single session across all of the subdomains.
I would like to somehow securely transmit auto login request.
Possible Solution
I have considered using a single-use, random token that I would store in a cookie and in the users table. After the user successfully creates an account, he would be redirected to the subdomain. At that point the token would be consumed/destroyed and the user would be automatically logged in.
I would also need to have a short window for the token to be used before expiring.
Thoughts? Thanks!
I had the same issue, the possible solution you suggest does not work because the session is not shared between subdomains.
I solved it the following way (same idea you propossed, different implementation):
Create a new model (I called it LoginKey) that contains the user_id and a random SHA1 key.
When the user is authenticated at the parent domain (for example: mydomain.com/users/sign_in), a new LoginKey is created and the user is redirected to the corresponding subdomain to an action that I called login_with_key (for example: user_subdomain.mydomain.com/users/login_with_key?key=f6bb001ca50709efb22ba9b897d928086cb5d755322a3278f69be4d4daf54bbb)
Automatically log the user in with the key provided:
key = LoginKey.find_by_login_key(params[:key])
sign_in(key.user) unless key.nil?
Destroy the key:
key.destroy
I didn't like this solution 100%, I tried out a lot of different approaches that do not require a db record to be created, but always faced security concerns, and I think this one is safe.

Two tier sign in with Devise (Amazon style)

Let's imagine I have following scenario
User receives an email that there is a new item waiting for her
Clicks on a link and is able to either confirm or reject item (details skipped)
Can then access a list of all her items
The trick is that I would like to allow all this happen without user signing in but then limit access to other parts of the website (like sending an item to another user)
How I see it is that:
when user clicks a link she is signed in but only on tier 1 - with access only to confirm/reject action and read only to index of items (that's when Devise session is created)
when user wants to access other part of the website the sign in page is presented
when user comes to the website just by typing in the url http://example.com and wants to access own account she is asked to sign in.
after sign in session is "promoted" to tier which allows full access
after some time of inactivity session is downgraded to tier 1 for security reasons
My inspiration comes from how Amazon works - you can access in read-only most parts of the account but before performing any destructible actions you need to sign in.
Does anyone have any experience with such approach or can share some blog posts, etc?
I didn's find anything on SO and Google mostly returned things about two-factor auth which is not the case here.
I also understand that there are security concerns with links in email.
I have implemented a very similar behavior few months ago. I don't have very interesting resources to show you but I can explain a bit how you could organize or think about the problem to solve.
Description
For the problem you state, it looks like once you have identified a user, you have two different states you can give him:
limited access (perform certain actions, read most of the resources, etc)
full access (allows them to do anything they would normally do).
Having stated that, what you need to do is figure out in which cases you will give a user each access state (for example):
signing in with email token -> limited access
password -> full access
authentication_token -> full access
omniauth -> full access
After that, you will need to save this information in the user session. This should be done anytime the user is authenticated, as you will know what strategy was used to authenticate the user.
To know if a user can or cannot perform an action you will need two things, know what the user can do, and the current "access state". Depending on those you will decide wether the user is allowed or not to perform a certain action.
Whenever a user can't perform an action and is logged in with limited access you should bring him to the flow for verifying his crendetials. This flow is pretty simple, almost like a sign in but just with the password. Once you verify his crendetials you can upgrade his authorization to a full access one.
Implementation details
I recommend you to create a Authorization model which will represent the "access states" that I mentioned. This model will have to be serialized in the session so you should be able to build it from a simple structure and serialize it again into that structure. The simplest the better (a boolean flag, an array or maybe a hash). For the case mentioned, it looks like a boolean would do the job.
Regarding implementation details, I recommend you implementing this with a Warden after_atuhentication callback.
You could implement this with CanCan by creating you own Ability that would be built with an Authorization instance and a User instance.
I think you're confusing authorization and authentication. Devise is an authentication solution, meaning it handles the "proof me you are who you say you are" part. Authorization is the "Ok, I know who you are, now let's see what can you do". Devise doesn't provide an authorization system beyond the simple "logged/not logged". If you need a more complex authorization system, use an authorization gem. CanCan is very popular.

Opening up part of a secured application without compromising the entire application

There's a subset of users which will not have access to the system I'm implementing in the beginning but I need a mechanism for them to capture data for one specific part of the process.
An authorized user creates the original record for a Person with some basic details i.e. First name, last name etc.
I then create a 'DataRequest' record which has a unique guid and the external user is sent an email with a path which is effectively http://sampleapplication/Person/Complete?guid=xxxx
The external user adds additional details like Date of Birth, Eye colour etc, submits and saves to the DB. The DataRequest for that guid is then expired and cannot be accessed again.
The Complete action doesn't have any authorization as these external users do not have user accounts.
My preference is to force these users to use the system but at this stage I'm not sure it's practical.
Is this a bad practice?
Should I be implementing some additional security on this like a one time password / passcode contained in the email? Are there alternative approaches I should consider?
There's nothing wrong with opening up a section of your site to the public. Tons of websites have secured and unsecured sections. However, there's also nothing saying that you have to expose your secure site at all. You can create another site that merely has access to that change those records and make that site alone, public.
As far as securing the information of the user, passcodes by email are the invention of some developer somewhere with limited mental ability or a severe lack of sleep. If the link is only available by email (not discoverable by search engines and not easily guessable), then anyone with the link will also have the passcode, making the passcode to access the link redundant.
You should however log when the email is used to finish the record and then disallow further uses.

Resources