I am in my first steps of implementing OpenID in my Rails app.
open_id_authentication appeared to be a fairly easy-to-use plugin, which is why I decided to use it.
Logging in with my Google account seems to work perfectly, however I do not get the sreg/AX fields that I require.
My code is currently as follows:
class SessionsController < ApplicationController
def new; end
def create
open_id_authentication
end
protected
def open_id_authentication
authenticate_with_open_id(params[:openid_identifier], :required => ["http://axschema.org/contact/email"]) do |result, identity_url, registration|
if result.successful?
p registration.data
#current_user = User.find_by_identity_url(identity_url)
if #current_user
successful_login
else
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
failed_login result.message
end
end
end
private
def successful_login
session[:user_id] = #current_user.id
redirect_to(root_url)
end
def failed_login(message)
flash[:error] = message
redirect_to(new_session_url)
end
end
I have already read various discussions about Google OpenID and all only say that you need to require the AX schema instead of the sreg field email, but even when I am doing so (as you can see in the code above), registration.data will remain empty ({}).
How do I effectively require the email from most OpenID providers with open_id_authentication?
The authenticate_with_open_id return the Sreg object, not the AX response. So you need instanciate this respone with Rack::OpenID::REPONSE like that :
ax_response = OpenID::AX::FetchResponse.from_success_response(request.env[Rack::OpenID::RESPONSE])
After you can fetch your data
ax_response['http://axschema.org/contact/email']
ax_response['http://axschema.org/namePerson/first']
ax_response['http://axschema.org/namePerson/last']
I've also stitched together a complete solution to Ruby on Rails 3, OpenID, and Google: http://blog.sethladd.com/2010/09/ruby-rails-openid-and-google.html
this post contains a good strategy to use AX for google and Sreg for others, to make this happen a little more seamlessly
http://www.franzens.org/2009/01/using-google-federated-login-in-your.html
Related
My app is using Rails 3.0.4 and Devise 1.1.7.
I'm looking for a way to prevent users from sharing accounts as the app is a subscription based service. I've been searching for over a week, and I still don't know how to implement a solution. I'm hoping someone has implemented a solution and can point me in the right direction.
Solution (Thank you everyone for your answers and insight!)
In application controller.rb
before_filter :check_concurrent_session
def check_concurrent_session
if is_already_logged_in?
sign_out_and_redirect(current_user)
end
end
def is_already_logged_in?
current_user && !(session[:token] == current_user.login_token)
end
In session_controller that overrides Devise Sessions controller:
skip_before_filter :check_concurrent_session
def create
super
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:token] = token
current_user.login_token = token
current_user.save
end
In migration AddLoginTokenToUsers
def self.up
change_table "users" do |t|
t.string "login_token"
end
end
def self.down
change_table "users" do |t|
t.remove "login_token"
end
end
This gem works well: https://github.com/devise-security/devise-security
Add to Gemfile
gem 'devise-security'
after bundle install
rails generate devise_security:install
Then run
rails g migration AddSessionLimitableToUsers unique_session_id
Edit the migration file
class AddSessionLimitableToUsers < ActiveRecord::Migration
def change
add_column :users, :unique_session_id, :string, limit: 20
end
end
Then run
rake db:migrate
Edit your app/models/user.rb file
class User < ActiveRecord::Base
devise :session_limitable # other devise options
... rest of file ...
end
Done. Now logging in from another browser will kill any previous sessions. The gem actual notifies the user that he is about to kill a current session before logging in.
You can't do it.
You can control IP addresses of user, so you can prevent presence of user from two IP at a time. ANd you can bind login and IP. You can try to check cities and other geolocation data through IP to block user.
You can set cookies to control something else.
But none of this will guarantee that only one user uses this login, and that those 105 IP from all over the world doesn't belong to only one unique user, which uses Proxy or whatever.
And the last: you never need this in the Internet.
UPD
However, what I'm asking is about limiting multiple users from using the same account simultaneously which I feel should be possible
So you can store some token, that will contain some encrypted data: IP + secret string + user agent + user browser version + user OS + any other personal info: encrypt(IP + "some secret string" + request.user_agent + ...). And then you can set a session or cookie with that token. And with each request you can fetch it: if user is the same? Is he using the same browser and the same browser version from the same OS etc.
Also you can use dynamic tokens: you change token each request, so only one user could use system per session, because each request token will be changed, another user will be logged out as far as his token will be expired.
This is how I solved the duplicate session problem.
routes.rb
devise_for :users, :controllers => { :sessions => "my_sessions" }
my_sessions controller
class MySessionsController < Devise::SessionsController
skip_before_filter :check_concurrent_session
def create
super
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:token] = token
current_user.login_token = token
current_user.save(validate: false)
end
end
application_controller
def check_concurrent_session
if duplicate_session?
sign_out_and_redirect(current_user)
flash[:notice] = "Duplicate Login Detected"
end
end
def duplicate_session?
user_signed_in? && (current_user.login_token != session[:token])
end
User model
Add a string field via a migration named login_token
This overrides the default Devise Session controller but inherits from it as well. On a new session a login session token is created and stored in login_token on the User model. In the application controller we call check_concurrent_session which signs out and redirects the current_user after calling the duplicate_session? function.
It's not the cleanest way to go about it, but it definitely works.
As far as actually implementing it in Devise, add this to your User.rb model.
Something like this will log them out automatically (untested).
def token_valid?
# Use fl00rs method of setting the token
session[:token] == cookies[:token]
end
## Monkey Patch Devise methods ##
def active_for_authentication?
super && token_valid?
end
def inactive_message
token_valid? ? super : "You are sharing your account."
end
I found that the solution in the original posting did not quite work for me. I wanted the first user to be logged out and a log-in page presented. Also, the sign_out_and_redirect(current_user) method does not seem to work the way I would expect. Using the SessionsController override in that solution I modified it to use websockets as follows:
def create
super
force_logout
end
private
def force_logout
logout_subscribe_address = "signout_subscribe_response_#{current_user[:id]}"
logout_subscribe_resp = {:message => "#{logout_subscribe_address }: #{current_user[:email]} signed out."}
WebsocketRails[:signout_subscribe].trigger(signout_subscribe_address, signout_subscribe_resp)
end
end
Make sure that all web pages subscribe to the signout channel and bind it to the same logout_subscribe_address action. In my application, each page also has a 'sign out' button, which signs out the client via the devise session Destroy action. When the websocket response is triggered in the web page, it simply clicks this button - the signout logic is invoked and the first user is presented with the sign in page.
This solution also does not require the skip_before_filter :check_concurrent_session and the model login_token since it triggers the forced logout without prejudice.
For the record, the devise_security_extension appears to provide the functionality to do this as well. It also puts up an appropriate alert warning the first user about what has happened (I haven't figured out how to do that yet).
Keep track of uniq IPs used per user. Now and then, run an analysis on those IPs - sharing would be obvious if a single account has simultaneous logins from different ISPs in different countries. Note that simply having a different IP is not sufficient grounds to consider it shared - some ISPs use round-robin proxies, so each hit would necessarily be a different IP.
While you can't reliably prevent users from sharing an account, what you can do (I think) is prevent more than one user being logged on at the same time to the same account. Not sure if this is sufficient for your business model, but it does get around a lot of the problems discussed in the other answers. I've implemented something that is currently in beta and seems to work reasonably well - there are some notes here
I'm trying to implement a 3 legged authentication in my app to the Google API, to be able to access registered users' Google Calendars.
In the quickstart Ruby guide, this command comes up that as far as I understand should point to the user's tokens:
token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
It expects the tokens stored in a file (or Redis), but (of course) I store each user's tokens in my database (Postgres).
Have I understand the purpose of the command wrongly or otherwise - how do I use it with a database store?
Official documentation
I implemented it myself based on #Rafe's answer. Just wanted to share in case someone wants to copy the ActiveRecord / Database store implementation:
module Google
module Auth
module Stores
class DatabaseTokenStore < Google::Auth::TokenStore
def load(id)
user = User.find(id)
{
"client_id": ENV['google_client_id'],
"access_token": user.token,
"refresh_token": user.refresh_token,
"scope": ENV['google_scopes'],
"expiration_time_millis": user.token_expires_at
}.to_json
end
def store(id, token)
user = User.find(id)
hsh = JSON.parse(token)
user.update(
token: hsh["access_token"],
token_expires_at: hsh["expiration_time_millis"] / 1000
)
end
end
end
end
end
Implement it yourself, according to the the readme:
Custom storage implementations can also be used. See token_store.rb for additional details.
It shouldn't be too hard to implement the load(id), store(id, token) and delete(id) with ActiveRecord (or another ORM) by the looks of the mentioned files.
Accepted answer above it's good and I recommend it https://stackoverflow.com/a/48267763/473040, but I find it useful to store everything for future debugging. Also simplicity is beautiful :)
Add json column to Postgres DB table (or serialise text field in other db)
class AddGooglePhotosTokensToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :google_photo_tokens, :json
end
end
class:
class GoogleAuthDbStore < Google::Auth::TokenStore
def load(id)
user = User.find(id)
user.google_photo_tokens
end
def store(id, tokens)
user = User.find(id)
user.google_photo_tokens = tokens
user.save!
end
def delete id
user = User.find(id)
user.google_photo_tokens = nil
user.save!
end
end
use
def authorizer
return #authorizer if #authorizer
client = Google::Auth::ClientId.new(Rails.application.credentials.google.fetch(:client_id), Rails.application.credentials.google.fetch(:client_secret))
scope = ['https://www.googleapis.com/auth/photoslibrary.readonly']
token_store = Provider::GoogleAuthDbStore.new # <<<here
#authorizer = Google::Auth::WebUserAuthorizer.new(client, scope, token_store, '/auth/google/callback')
#authorizer
end
I am working on some Cucumber stories for a 'sign up' application which has a number of steps.
Rather then writing a Huuuuuuuge story to cover all the steps at once, which would be bad, I'd rather work through each action in the controller like a regular user. My problem here is that I am storing the account ID which is created in the first step as a session variable, so when step 2, step 3 etc are visited the existing registration data is loaded.
I'm aware of being able to access controller.session[..] within RSpec specifications however when I try to do this in Cucumber stories it fails with the following error (and, I've also read somewhere this is an anti-pattern etc...):
Using controller.session[:whatever] or session[:whatever]
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
Using session(:whatever)
wrong number of arguments (1 for 0) (ArgumentError)
So, it seems accession the session store isn't really possible. What I'm wondering is if it might be possible to (and I guess which would be best..):
Mock out the session store etc
Have a method within the controller and stub that out (e.g. get_registration which assigns an instance variable...)
I've looked through the RSpec book (well, skimmed) and had a look through WebRat etc, but I haven't really found an answer to my problem...
To clarify a bit more, the signup process is more like a state machine - e.g. the user progresses through four steps before the registration is complete - hence 'logging in' isn't really an option (it breaks the model of how the site works)...
In my spec for the controller I was able to stub out the call to the method which loads the model based on the session var - but I'm not sure if the 'antipattern' line also applies to stubs as well as mocks?
Thanks!
I'll repeat danpickett in saying mocks should be avoided whenever possible in Cucumber. However if your app does not have a login page, or perhaps performance is a problem, then it may be necessary to simulate login directly.
This is an ugly hack, but it should get the job done.
Given /^I am logged in as "(.*)"$/ do |email|
#current_user = Factory(:user, :email => email)
cookies[:stub_user_id] = #current_user.id
end
# in application controller
class ApplicationController < ActionController::Base
if Rails.env.test?
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
end
mocks are bad in cucumber scenarios - they're almost kind of an antipattern.
My suggestion is to write a step that actually logs a user in. I do it this way
Given I am logged in as "auser#example.com"
Given /^I am logged in as "(.*)"$/ do |email|
#user = Factory(:user, :email => email)
#user.activate!
visit("/session/new")
fill_in("email", :with => #user.email)
fill_in("password", :with => #user.password)
click_button("Sign In")
end
I realize that the instance variable #user is kind of bad form—but I think in the case of logging in/out, having #user is definitely helpful.
Sometimes I call it #current_user.
Re. Ryan's solution - you can open up ActionController in you env.rb file and place it there to avoid putting in your production code base (thanks to john # pivotal labs)
# in features/support/env.rb
class ApplicationController < ActionController::Base
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
I don't know how much this relates to the original question anymore, but I decided to post anyway in the spirit of discussion...
We have a cucumber test suite that takes > 10 minutes to run so we wanted to do some optimization. In our app the login process triggers a LOT of extra functionality that is irrelevant to majority of the scenarios, so we wanted to skip that by setting the session user id directly.
Ryanb's approach above worked nicely, except that we were unable to log out using that approach. This made our multi-user stories fail.
We ended up creating a "quick login" route that is only enabled in test environment:
# in routes.rb
map.connect '/quick_login/:login', :controller => 'logins', :action => 'quick_login'
Here is the corresponding action that creates the session variable:
# in logins_controller.rb
class LoginsController < ApplicationController
# This is a utility method for selenium/webrat tests to speed up & simplify the process of logging in.
# Please never make this method usable in production/staging environments.
def quick_login
raise "quick login only works in cucumber environment! it's meant for acceptance tests only" unless Rails.env.test?
u = User.find_by_login(params[:login])
if u
session[:user_id] = u.id
render :text => "assumed identity of #{u.login}"
else
raise "failed to assume identity"
end
end
end
For us this ended up being simpler than working with the cookies array. As a bonus, this approach also works with Selenium/Watir.
Downside is that we're including test-related code in our application. Personally I don't think that adding code to make application more testable is a huge sin, even if it does add a bit of clutter. Perhaps the biggest problem is that future test authors need to figure out which type of login they should use. With unlimited hardware performance we obviously wouldn't be doing any of this.
Re: Ryan's solution:
Does not work with Capybara, unless small adaptation done:
rack_test_driver = Capybara.current_session.driver
cookie_jar = rack_test_driver.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
#current_user = Factory(:user)
cookie_jar[:stub_user_id] = #current_user.id
(found here: https://gist.github.com/484787)
My understanding is that you get:
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
when session[] is accessed before request has been instantiated. In your case, I'd imagine if you put webrats' visit some_existing_path before accessing session[] in your step defenition, the error will go away.
Now, unfortunately, session doesn't seem to persist across steps (at least, I couldn't find the way), so this bit of information doesn't help to answer your question :)
So, I suppose, Ryan's session[:user_id] = cookies[:stub_user_id]... is the way to go. Although, imo, test related code in the application itself doesn't sound right.
I use a testing-only sign-in solution like Prikka's, but I do it all in Rack instead of creating a new Controller and routes.
# in config/environments/cucumber.rb:
config.middleware.use (Class.new do
def initialize(app); #app = app; end
def call(env)
request = ::Rack::Request.new(env)
if request.params.has_key?('signed_in_user_id')
request.session[:current_user_id] = request.params['signed_in_user_id']
end
#app.call env
end
end)
# in features/step_definitions/authentication_steps.rb:
Given /^I am signed in as ([^\"]+)$/ do |name|
user = User.find_by_username(name) || Factory(:user, :username => name)
sign_in_as user
end
# in features/step_definitions/authentication_steps.rb:
Given /^I am not signed in$/ do
sign_in_as nil
end
module AuthenticationHelpers
def sign_in_as(user)
return if #current_user == user
#current_user = user
get '/', { 'signed_in_user_id' => (user ? user.to_param : '') }
end
end
World(AuthenticationHelpers)
#Ajedi32 I ran into the same issue (undefined method 'current_session' for Capybara::RackTest::Driver) and putting this in my step definition fixed the problem for me:
rack_test_browser = Capybara.current_session.driver.browser
cookie_jar = rack_test_browser.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
cookie_jar[:stub_user_id] = #current_user.id
In my controller action, I referred to cookies[:stub_user_id], instead of cookie_jar[:stub_user_id]
Why don't you use FactoryGirl or (Fixjour or Fabricator) with Devise (or Authlogic) and SentientUser? Then you can simply sniff which user is already logged in!
#user = Factory(:user) # FactoryGirl
sign_in #user # Devise
User.current.should == #user # SentientUser
Another slight variation:
# In features/step_definitions/authentication_steps.rb:
class SessionsController < ApplicationController
def create_with_security_bypass
if params.has_key? :user_id
session[:user_id] = params[:user_id]
redirect_to :root
else
create_without_security_bypass
end
end
alias_method_chain :create, :security_bypass
end
Given %r/^I am logged in as "([^"]*)"$/ do |username|
user = User.find_by_username(username) || Factory(:user, :username => username)
page.driver.post "/session?user_id=#{user.id}"
end
After a lot of soul searching and web surfing, I finally opt'ed for a very simple and obvious solution.
Using cookies adds two problems. First you have code in the application specific for testing and second there is the problem that creating cookies in Cucumber is hard when using anything other than rack test. There are various solutions to the cookie problem but all of them are a bit challenging, some introduce mocks, and all of them are what I call 'tricky'. One such solution is here.
My solution is the following. This is using HTTP basic authentication but it could be generalized for most anything.
authenticate_or_request_with_http_basic "My Authentication" do |user_name, password|
if Rails.env.test? && user_name == 'testuser'
test_authenticate(user_name, password)
else
normal_authentication
end
end
test_authenticate does what ever the normal authenticate does except it bypasses any time consuming parts. In my case, the real authentication is using LDAP which I wanted to avoid.
Yes… it is a bit gross but it is clear, simple, and obvious. And… no other solution I've seen is cleaner or clearer.
Note, one feature is that if the user_name is not 'testuser', then the normal path is taken so they can be tested.
Hope this helps others...
I'm building a marketplace application that uses PayPal Express. I've got a form for sellers to input their PayPal API credentials, but I need a way to validate them by making some sort of call to PayPal.
I'm using the PaypalExpressGateway in ActiveMerchant, and I don't see anything other than the standard purchase controls. Is there any sort of null-operation that can be used?
Any help would be greatly appreciated!
I am using the TransactionSearch operation for this purpose. By specifying STARTDATE=2100-01-01 00:00:00 it basically results in a no-op.
It will validate the credentials for you, without requiring any additional input from the seller.
For security reasons there isn't a way to check if the email is a valid paypal account. You can always make a small transaction and then void it if account validity is really required.
I don't have the answer personally. But I know Ryan Bates of Railscasts.com has recently devoted six (!) episodes to ActiveMerchant and Paypal in particular. Check out episodes #141 through #146 at railscasts.com.
Ok, after 4 hours...
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class PaypalExpressGateway < Gateway
def get_balance(options = {})
commit 'GetBalance', build_get_balance_request(options)
end
private
def build_get_balance_request(options)
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'GetBalanceReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'GetBalanceRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! 'n2:Version', API_VERSION
xml.tag! 'n2:ReturnAllCurrencies', '1'
end
end
xml.target!
end
end
end
end
class SellerMerchantValidator < ActiveModel::Validator
def validate(record)
paypal_attrs = ['paypal_api_username', 'paypal_api_password', 'paypal_api_signature']
if record.paypal_merchant? && (record.changed - paypal_attrs).size < record.changed.size # one of paypal_attrs changed
response = record.gateway.get_balance
unless response.params['balance'].present?
record.errors[:base] << "Please check the PayPal details and make sure all three are entered correctly."
end
end
end
end
Thanks to Neils for the idea to check the TransactionSearch.
Please let me know if there is a better way to check if any of the api field changed.
There is also a call for GetBalance in the API.
Some sample code
Looks like the simplest (and quickest?) way.
Right, so if you want to test a user's credentials using ActiveMerchant, use the transaction_search method on the gateway
https://github.com/Shopify/active_merchant/blob/cb72e0f9c58f57b1293e6e976229b26cfbfee6a8/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb
This example will return a success (make sure to fill in your test credentials)
#username = ''
#password = ''
#signature = ''
gateway = ActiveMerchant::Billing::PaypalExpressGateway.new(
login: #username,
password: #password,
signature: #signature,
test: true
)
gateway.transaction_search({start_date: DateTime.now})
PayPal does have an AddressVerify API. It confirms whether a postal address and postal code match those of the specified PayPal account holder. I'm in the process of implementing it on our website right now, in fact.
You can read more about it here:
https://www.x.com/docs/DOC-1162#id0862M0QH02L
and here:
https://cms.paypal.com/us/cgi-bin/?&cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_AddressVerify
I'm trying to write a Cucumber scenario that requires me to have a logged in user - that would normally be quite simple but I'm only using OpenID authentication (curtosy of the authentication plugin). However after digging through the open_id_authentication plugins guts I'm not sure how I could achieve this within Cucumber.
I've figured out a way, if you place this in your features/support/env.rb:
ActionController::Base.class_eval do
private
def begin_open_id_authentication(identity_url, options = {})
yield OpenIdAuthentication::Result.new(:successful), identity_url, nil
end
end
Then you can just do something like this in your appropriate step:
Given /^I am logged in as "(.*)"$/ do |name|
user = User.find_by_name(user)
post '/session', :openid_url => user.identity_url
# Some assertions just to make sure our hack in env.rb is still working
response.should redirect_to('/')
flash[:notice].should eql('Logged in successfully')
end
I'm just completely clobbering the open id auth for the cucumber features, obviously if I need instances where there is failed login I could do that based on the supplied identity_url.
If you want to be able to stub out responses do this:
In features/support/helpers.rb:
ActionController::Base.class_eval do
private
def fake_openid_response(identity_url)
[OpenIdAuthentication::Result.new(:successful), identity_url, nil]
end
def begin_open_id_authentication(identity_url, options = {})
yield fake_openid_response(identity_url)
end
end
By moving the response out to a separate method you can now stub the response in your steps if necessary. For example, if I wanted a :missing response and I had a controller GoogleLoginController I could do the following using Mocha:
GoogleLoginController.any_instance.stubs(:fake_openid_response)
.returns([OpenIdAuthentication::Result.new(:missing), identity_url, nil])
Bort, a rails skeleton app, has a full set of rspec tests and supports openID login so you may want to take a look and see what they do.
DEfusion's answer works except that I needed to normalize the identity_url like:
ActionController::Base.class_eval do
private
def begin_open_id_authentication(identity_url, options = {})
yield OpenIdAuthentication::Result.new(:successful), self.normalize_identifier(identity_url), nil
end
end
Thanks