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...
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 am trying to create a rails 4 app with omniauth devise :
signin with facebook
signin with google
signin with linkedin
signin with twitter
Here, I am able to login in with either facebook, linkedin, twitter or google account. But my problem is: my google account email and linkedin email address are same. And login with google and then login with linkedin is giving me this error:
"Validation failed: Email has already been taken"
This is a problem because devise uses :unique => true in migration file for email field.
Can anyone provide me nice idea to handle this error please?
I think that basically, if handling all the cases properly, this can be really complex unless you choose option 1 below (and even then, there are issues to consider which I outline afterwards). Sorry for the length of this answer!
I'm assuming you've done something like this:
https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview
so far, which gets you some of the way but doesn't handle the problem you're encountering.
The way I have done this, I have a User who has_many Identities. Identity stores the name of the external service, the user id it tells you and whatever else you want. This means that the same user can log in with multiple identities (twitter, Facebook…). Have you seen:
http://railscasts.com/episodes/235-omniauth-part-1?view=asciicast
http://railscasts.com/episodes/236-omniauth-part-2?view=asciicast
which help with getting going with the User has_many Identities, but still don't deal with your case.
Then to solve your issue, one option is to detect the validation error you're encountering:
if #user.errors.added?(:email, :taken)
# do whatever you want - e.g. one of the 4 options below.
end
and if it occurs, you could either:
just add the identity to identities associated with that existing user who has the same email address and then sign them in.
or
before adding the identity to the existing user, ask for the password for the existing user account (if the account was originally registered via devise on your system), so you'll need to go to some new controller/action and view that handles this flow.
or
Perhaps send an email confirmation (not devise's standard confirmation) to acknowledge that they are linking their new identity to an existing account. This sounds a bit complicated, because you'll have to store the identity temporarily somewhere (probably in a database if you want to cope with them ending their current session before clicking a confirmation link), flagging it as unconfirmed, until they click a confirmation link in an email (which you'll also have to deal with generating).
or
Perhaps force them to authenticate with the other identity that has the same email address. This has the advantage over the previous option that you can just save the new identity info in the session and get them to authenticate using the other service immediately, but obviously there will be some work to handle the flow there.
Option 1 can be less secure because you are trusting that external services have confirmed the user's email address - which perhaps they have - but you want to be sure otherwise someone could sign up with linkedin using their email address and then sign in to your site and then an attacker could sign up with another service but using the same email address. They could then access the person's account on your site if you didn't confirm they really owned the email address somehow (e.g. by using option 2, 3 or 4). If the external services can confirm they've verified the email addresses, then this option should be ok and is by far the simplest - for example, Facebook have a field that tells you the account has been verified (but see my comments below about services that don't need email addresses). If they're merging with an account registered with you directly (doesn't sound like your situation), you should have confirmed the email address they registered using devise's standard Confirmable feature.
Option 2 sounds like it doesn't apply in your case, because you don't mention that a user can register with you directly via devise; only sign in using external services. This means that they have no password on your site that they know. (You've probably added a dummy one to get the devise validation to pass, unless you've disabled that, but they won't know the password unless you've told them somehow, and it could be confusing to them to do that).
Option 3 sounds do-able, though I haven't tried it. It's a bit more laborious for the user.
Option 4 also sounds do-able, though I haven't tried it either.
So far, I've done option 2 because users can only register with my site either directly using devise or via 1 external service. I'll be adding other services soon, so I plan to use option 4 (perhaps only if external service says they haven't confirmed the email address, and option 1 otherwise).
Options 2, 3 and 4 are a fair bit more work than option 1, so it depends if you can confirm that the external services have verified the email addresses and if not, how paranoid you are about attackers being able to access user accounts on your site. Personally, I err on the side of paranoia!
This might also give you some more useful info:
https://github.com/intridea/omniauth/wiki/Managing-Multiple-Providers
but because omniauth itself doesn't concern itself with model issues, it mostly sidesteps it, though it says for your case that it is "probably sufficiently prudent to assume that they are, in fact, the same person who also created the previous user" but you have to be able to trust the external services as I mentioned above.
There are also other things to consider, such as the case where someone has the same email address registered with Facebook and linked in and has signed in with both on your site (so single user account once you've dealt with your issue) and then changes the email associated with their Facebook account but not linkedin. If you always overwrite the email stored in the user table with the one from the external service, then it'll keep changing if they log in with linkedin and then Facebook (but maybe this doesn't matter to you). Alternatively, they may have different email addresses registered with Facebook and linked in and have logged in with both on your site (so 2 different users on your site) and then they change their linked in email address to be the same as the Facebook one. If you update the email address for a user every time they log in via an external service, you'll have your "Email already taken" error, but in this case you have 2 existing users to merge which could be interesting depending on what else in your database is associated with a user...
Also, I don't think twitter returns an email address, so if the same person has logged in with twitter and linkedin, you won't detect this. Furthermore, I think email is optional with Facebook (you can use a mobile phone number), so the same thing can happen with Facebook. My ideal solution would allow the user to merge arbitrary accounts, obviously having to enter whatever credentials are required to confirm they own the accounts they are merging! I haven't done this yet, but it's on my wish list!
I followed these steps and working fine for me:
1. Gemfile
gem 'omniauth-facebook', '1.4.0'
gem 'omniauth-twitter'
gem 'omniauth-google-oauth2'
2. config/route.rb
devise_for :users, controllers: { omniauth_callbacks: "omniauth_callbacks" }
3. Links
<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
<%= link_to "Sign in with twitter", user_omniauth_authorize_path(:twitter) %>
<%= link_to "Sign in with google", user_omniauth_authorize_path(:google_oauth2) %>
4. controllers/omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :authenticate_user!
def all
user = User.from_omniauth(request.env["omniauth.auth"], current_user)
if user.persisted?
flash[:notice] = "you are successfully logged in!!"
sign_in_and_redirect(user)
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
def failure
super
end
alias_method :facebook, :all
alias_method :twitter, :all
alias_method :google_oauth2, :all
end
5. add required fields and model
rails g migration add_social_network_info_columns_to_users name image_url locations
# generate new model Authorization
rails g model Authorization user_id:integer provider uid token secret username
6. models/User.rb
class User < ActiveRecord::Base
require 'securerandom'
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable
has_many :authorizations
# omniauth facebook provider
def self.from_omniauth(auth, current_user)
# check for existing authorization
# Find or create Authorization with: provider, uid, token and secret
authorization = Authorization.where(
:provider => auth.provider,
:uid => auth.uid.to_s,
:token => auth.credentials.token,
:secret => auth.credentials.secret
).first_or_initialize
if authorization.user.blank?
user = current_user.nil? ? User.where('email = ?', auth["info"]["email"]).first : current_user
# save user related data in user table
if user.blank?
User.new(
:email => auth.info.email,
:password => Devise.friendly_token[0,10],
:name => auth.info.name,
:locations => auth.info.location,
:image_url => auth.info.image
)
# since twitter don't provide email,
# so you need to skip validation for twitter.
auth.provider == "twitter" ? user.save!(:validate => false) : user.save!
end
# store authorization related data in authorization table
authorization.username = auth.info.nickname
authorization.user_id = user.id
authorization.save!
end
authorization.user
end
end
6. model/Authorization.rb
class Authorization < ActiveRecord::Base
belongs_to :user
end
source:
https://github.com/mohitjain/social-login-in-rails
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".
I am using omniauth with LinkedIn as a provider. LinkedIn doesn't supply
an email in info hash, so i cannot provide an email when create the user based
on the information I get back.
Two related questions:
1) How can I adjust devise so that there isn't a requirement
for :email as a validation? It doesn't appear to be set under the
User model.
2) I do want to get the email information, however, so want to have
email information requested before creating the User. How can I
redirect to a page/wizard asking for email information and then come
back to finish the user registration?
I just solved this without needing to use the separate 'linkedin' gem, it was pretty difficult as there was a distinct lack of documentation on the subject!
Firstly you need to make the email-address available by adding the fields option to your LinkedIn Omniauth configuration, you will also need to override the request_token_path in order to add the r_emailaddress scope required to retrieve a users email address.
Mine ended up looking something like this (NB. Ruby 1.9.3):
provider :linkedin, external_services['linkedin']['api_key'], external_services['linkedin']['api_secret'], client_options: {request_token_path: '/uas/oauth/requestToken?scope=r_emailaddress'}, fields: ['id', 'first-name', 'last-name', 'headline', 'industry', 'picture-url', 'public-profile-url', 'email-address']
NOTE: Dont forget to change external_services['linkedin']['api_key'] and external_services['linkedin']['api_secret'] to your own.
Your user will then be asked to authorise use of their email address as well as their basic provide and you will have access to it once they are returned via:
auth['extra']['raw_info']['emailAddress']
I should probably commit this change back to omniauth-linkedin so you can simply set scope: r_emailaddress in the provider options, avoid the duplication of the field names and get the email back in the info section of the auth object.
If I get time after this section of my project is finished I will.
Take a look at the railscasts about omniauth: http://railscasts.com/episodes?utf8=%E2%9C%93&search=omniauth
The idea is the following:
Create a new user from the omniauth info
try to save the user
since the email is not present it won't validate
store the omniauth data in the session and redirect_to new_user_registration_url
create your own registration controller that inheritates from the devise one
override the build_resource(*args) method, and if the omniauth data is present, use it to create the resource (User in your case)
That way, after trying to login with linkedin, the user will be redirected to a form where he will be able to enter his email.
It's all explained in the railscast ;)
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.