I'm putting together a small app which will allow a user to log into a semi-popular social networking site that doesn't have a sufficient API, so I'm using the mechanize gem to automate a few functions I wanted to add for anyone to use, such as bulk messaging.
Because of the API restrictions, I'm forced to do this by pretending to be a user interacting with the http interface of the site.
The problem I'm having is once a user logs in to my site, how can I keep the cookie from the social networking site statefully in the session so they don't need to enter credentials for every page load?
I was trying to marshal the cookie from the mechanize instance but I received errors regarding mutex's not having a Marshal method.
EDIT - Solved
Turned out to be quite simple, but the documentation sure didn't help the matter, just because of incompleteness.
Here's what I did to catch the cookies and bring them back for future requests (much inbetween code and error checking excluded for brevity):
users_controller.rb
def create
session.delete(:agent) if session[:agent]
agent = Mechanize.new
page = agent.get('www.socialnetwork.com/login')
login_form = page.form
login_form.email = params[:login][:email]
login_form.password = params[:login][:password]
page = agent.submit(login_form, login_form.buttons.first)
cookies = agent.cookie_jar.store.map {|i| i}
session[:agent] = Marshal.dump cookies
redirect_to root_path
end
application_controller.rb
before_filter :set_agent
def set_agent
if session[:agent]
#agent = Mechanize.new
cookies = Marshal.load session[:agent]
cookies.each {|i| #agent.cookie_jar.store.add i}
end
end
Don't store the agent, just the session cookie. Find the name of the cookie you want to capture. After logging in, grab it from the Mechanize cookie jar, store it in the session, and make sure it's in the cookie jar before every "API request".
Related
On backend in a controller, I want to log in a user. Then I want to render a view render(view: '/my-view') where the user will be authenticated already.
Scenario
A user is given a link.
He goes to this link.
Backend redirects the link to a controller.
Controller creates a temporary account for the user and authenticates him.
Controller renders a view and ???? somehow sends the session to frontend ????.
How can I send the session to the frontend?
Define front end ?
Backend redirects the link a controller. Controller creates a
temporary account for the user and authenticates him.
This is how I am doing it sockets does authentication, access that user's http session from backend and puts in there that they have logged in. I then send a socket trigger back to front end html to say all ok
at this point gsp gets response from sockets and says aha redirect to /site/hello
Controller renders a view and ???? somehow sends the session to frontend ????
This /site/hello now checks for specific session and well user is also now logged in too.. the session details was set by backend when user authenticated and not front end session
in gsp you can do
<g:set var="something" scope="session"/>
But I think what i have described is what you need to do
If you need helping user session details it is all quite easy i don't have it to hand
but from gsp when connecting to sockets i send '${session.id}' which then i look up and bind back to user .....
Also note --- there is catch here, when user is not authenticated they have primary session, when they authenticate through spring security they are actually given a new session id. This is due to security issues but I have got around that with checking session.username which i set upon login and this now matches '${params.encryptedUsername}' decrypted on backend..
Ahh it's rolling back.. there is a concurrent hashmap which contains username,session and from that When i get Decrypted.username I get hashMap which the value is user http session to which i poke and do things with ...
I can give you my code but then that is a lot of work above is the steps in one way of how you go about it
So to answer your question, this is under grails 3:
Enable Spring security session listener in application.groovy
grails.plugin.springsecurity.useSecurityEventListener = true
Add CustomSecurityEventListener.groovy class to your app, remove the loginCacheAttempt, unless you wish to use it refer to build.gradle for that stuff and the related service etc in that demo app.
This then calls SessionListener provided in that same folder and adds user with session id to the sessions synchronised map declared at the top of SessionListener
Now in my websockets when I register a user:
String sessionId = userSession.userProperties.get("sessionId") as String
def userHttpsession = SessionListener.sessions.find{it.key==sessionId}?.value
userHttpsession.username = username
userHttpsession.password = password
This is still pre-authentication and primary session
I send a trigger to tell sockets to refresh gsp page to another window.location.href
In that location controller action i authenticate session details and invalidate session details
registerService.authenticateUser(user, session.password)
This way of doing things appears to work fine without the complications, there is an encrypted user which is sent as part of initial socket transaction to ensure/verify session.user matches encrypted user (for logged in user)
It seems like the programmatic login takes care of the session too.
springSecurityService.reauthenticate(email, password)
I'm testing my website using Devise, and what I've found is if I intercept the redirect after a Devise Sign Out, while the "destroy" method is being called, I can still use the back button and log back into the system.
A bit more details:
1) I am using Devise with the following options: :database_authenticatable, :recoverable, :trackable, :validatable, :lockable, :timeoutable, :password_archivable, :maximum_attempts => 4, :unlock_strategy => :none. Also, I using the default settings for sign_out_all_scopes (true).
2) I am using Burp Proxy to intercept calls coming back to the browser from the server.
3) My Session Controller Destroy method is as follows:
def destroy
Rails.logger.info "RESETTING SESSION"
reset_session
Rails.logger.info "DESTROYING DEVISE SESSION"
super
Rails.logger.info "DONE WITH DEVISE"
end
The logs calls are just for me, also I have tried with and without the reset_session.
4) I log in as a user, then I click my logout call, I see that the Destroy method is called on the Web Server (looking at the logs), but I intercept the call to the browser.
5) Clicking the back button, I am can get back into the site and I can navigate it like I never was logged out.
It appears to me that despite calling the Warden's logout behind the scenes, it actually do anything unless the cookie is destroyed on the browser. Also looking a little closer, it doesn't appear that Devise or Warden does anything to the database on logout meaning that logging out yet not destroying the cookie would have no effect anyway.
I'm relatively new to Rails and very new to Devise, so am I just missing something?
EDIT: So after a brief conversation with Billy Chen it seems I'm not doing anything wrong, this is just how Devise/Warden works.
I'm curious how anyone is solving this problem for sites where they want to be assured that logging out really logs out a user? Throwing in a status on the User object that gets updated on login/logout would be easy enough, but I'm curious why there is no option to do that in the current design (even if it was just optional).
EDIT 2:
Solved the issue. Despite not having :rememberable turned on in the User model, the devise.rb file had "config.use_salt_as_remember_token = true", removing this fixed the problem. Not exactly sure why that configuration should be used if the model doesn't allow rememberable, but at least the problem is solved.
EDIT 3:
Apparently I'm a liar, it's not fixed I had just missed intercepting the response in burp proxy. Retesting show this is still an issue. Fixing it by tagging the User with a key and removing it on logout. A hack, but it should fix the issue.
The "destroy" is to destroy session, not user. So why do you expect database operation in this "destroy" action?
When an user log in, he fill out email and password in login page, aka "Session#new".
When he hit "log in" button, the request is sent to "Session#create". If login info is correct, session established. The server send a cookie containing session id which is hash including user id.
On every requests from this user thereafter, his browser send request together with this cookie. The server decodes this hash and load user id from database.
If he wants to log out, he'll hit "logout" button which will send "DELETE" request to "Session#destroy", then server will ask his browser to delete the key/id from that cookie.
Now the session is destroyed. This user can no more access protected resources for login users, because requests he send thereafter won't contain his valid user info.
That's basically how it works. You can check the details here:
http://guides.rubyonrails.org/security.html
A user is logged in with traditional email and password.
That user clicks a "connect with Facebook" button.
They auth with facebook using a different email address than the one they're currently logged in to my site with.
I present them with a page asking if they want to update their traditional login to use their facebook email (also allowing me to unify the accounts and fill in any missing data using Facebook).
The problem is with step 4, as the omniauth callback URL hits sessions#create with the auth stored in request.env['omniauth.auth'].
Relevant bits of sessions_controller:
def create
return link_accounts if signed_in?
if params[:email]
create_session_from_login
elsif env['omniauth.auth']
create_session_from_facebook
else
redirect_to login_path, alert: "Couldn't login!"
end
end
private
def link_accounts
auth = env['omniauth.auth']
redirect_to user_link_accounts_path current_user unless current_user.email == auth[:info][:email]
end
... which takes me to my user controller, which displays a lovely little message prompting the user to either (a) sign into my site as another user, or (b) update their account to use this Facebook info... except that I no longer have access to request.env['omniauth.auth'].
How can I persist the auth info between requests? It's too big for session... I also want it someplace I can get to it after a user takes an action on a page (as in, clicking a link--so I can auth them server-side one request after the actual FB auth has taken place), but I don't want to store it on the page...
I am using the devise, omniauth, omniauth-twitter and twitter gems in a rails 3 app. I want to make it so when a user signs out it also removes the twitter gem configuration. What I'm referring to when I say "twitter gem configuration" is this:
Twitter.configure do |config|
config.consumer_key = YOUR_CONSUMER_KEY
config.consumer_secret = YOUR_CONSUMER_SECRET
config.oauth_token = YOUR_OAUTH_TOKEN
config.oauth_token_secret = YOUR_OAUTH_TOKEN_SECRET
end
If I don't do that and another user logs onto the app from the same computer but doesn't have a user account, they will see the previous user's twitter information. I believe I can remove the configuration by calling
Twitter.reset
I guess my question is where would be the best place to put that? Also if that isn't the best way to remove the user's twitter configuration, how should I do it?
Thanks and let me know if you need any more details.
You can split your configuration into application-specific (which will be global) and client-specific settings. This twitter gem wiki page describes this nicely.
This seems to be working.
In /app/controllers/application_controller.rb you can redirect the default route devise sends the user when they sign_in/out. I'm not sure if this is the "proper" place to put this but it seems reasonable.
class ApplicationController < ActionController::Base
protect_from_forgery
def after_sign_in_path_for(resource_or_scope)
if current_user.authentications.find_by_provider("twitter")
ckey= YOUR_APPS_CONSUMER_KEY
csecret= YOUR_APPS_CONSUMER_SECRET
auth = current_user.authentications.find_by_provider("twitter")
atoken = auth.token
asecret = auth.secret
Twitter.configure do |config|
config.consumer_key = ckey
config.consumer_secret = csecret
config.oauth_token = atoken
config.oauth_token_secret = asecret
end
end
authentications_path
end
def after_sign_out_path_for(resource_or_scope)
Twitter.reset
authentications_path
end
end
The authentications_path is just the page I'm using to test the authentications and related things. You can redirect anywhere. When a user links an account I save their oauth token and secret in the authentication object. You will need this to access certain aspects of the Twitter gem.
I'll wait a little while to mark this as the answer, see if someone has a better solution.
Here's my scenario.
First, I log in as Alice:
http://localhost:3000/?auth_token=eMXBk8cuJMA1ETZfMIgB
Then, without logging out, I log in as Bob:
http://localhost:3000?auth_token=Z9Ui7Cw_xCnOmGWOEUEH
What happens is, after the second GET request, I'm still logged in as Alice, not Bob.
If I do a http://localhost:3000/users/sign_out in between the two auth_token logins, everything's OK.
Without the sign_out, Bob can't login using his token.
Is this a bug, or the way things should be due to some security issues I'm ignorant of?
Can this behavior be overriden through hooks?
I've run into this with restful_authentication and devise. Here is what I use to handle it (put in application_controller.rb) and call it where needed. Note that I use :ak for the auth token. Change to whatever you're using.
def switch_session(api_key_passed)
if api_key_passed != current_user.authentication_token
logger.info("******Switching session as token is different.*******")
user = User.find_by_authentication_token(api_key_passed)
sign_out(user)
if #api_login_enabled.present?
redirect_to(new_user_session_path(:ak => api_key_passed))
else
logger.info("***API Login Setting is Disabled.***")
end
end
end
Devise's token_authenticatable strategy is a login path. Sending a User's authentication_token to Devise will log in that user and set a session, just as logging in via the web would. It is not supposed to act as an API Key, which would be required to be sent on every request and knowledge of that request disappears once the server responds.
Take a look at this issue here for more information: https://github.com/plataformatec/devise/issues/300
#jschorr's answer will work if you wish to use it more like an API key, but you should be aware that the original issue will not actually persist the previous user's session between different clients, this is not a security issue of sessions leaking between clients, and this is exactly how the authors of Devise intended. Just as you would need to log out of your Significant Other's webmail account in order to check your own if they just checked their mail from the same computer, you would need to send a logout message to your Rails app before you can switch accounts.
You are missing a setting on devise.rb initializer:
# By default Devise will store the user in session. You can skip storage for
# :http_auth and :token_auth by adding those symbols to the array below.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing :skip => :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:token_auth]
So no session is used when a user authenticates with an auth_token.