Having integrated merb_auth_password_slice as per the README, I can successfully login as redirect_after_login is being triggered, although session.authenticated? returns false.
Just trying the basic auth strategy for now (password form), can't seem to get it working, any ideas?
My init file:
require 'dm-validations'
dependencies "merb-more", "merb_helpers", "merb-slices", "merb_auth_password_slice"
Merb::BootLoader.before_app_loads do
DataMapper.setup(:default, "sqlite3://config/dev.db")
end
Merb::BootLoader.after_app_loads do
# have already done this
# raise "You must specify a valid openid in Merb.root/config/open_id to use this example app" unless File.exists?(Merb.root / "config" / "open_id")
# # DataMapper.auto_migrate!
# User.create(:login => "admin",
# :password => "password", :password_confirmation => "password",
# :email => "admin#example.com",
# :identity_url => File.read(Merb.root / "config" / "open_id"))
end
Merb::Config.use do |c|
c[:session_secret_key] = 'my key'
c[:session_store] = 'cookie'
end
Setup.rb
class Authentication
def store_user(user)
return nil unless user
user.id
end
def fetch_user(session_info)
User.get(session_info)
end
end # Authentication
# before(nil, :only => [:update, :destroy]) { session.abandon! }
This is the culprit in the slice's session controller
Related
I primarily come from a PHP and ASP.NET background. Recently I got involved with Ruby and am starting an interesting relationship with Padrino. Not too much like Rails and not too less like Sinatra.
I am making first serious application using Padrino and it didn't take long to get stuck and would appreciate your help.
The issue with what I believe is with Padrino Admin. I am trying make users login to my website using Facebook and Omniauth.
I have been following this tutorial: Padrino and Omniauth Overview.
The application is hosted at Heroku.
Result: On Facebook login, an account is crated ( in the database ). But when I reach the restricted area, I get redirected back to the login page.
Here is what I have.
app.rb
module PDeen
class App < Padrino::Application
register Padrino::Admin::AccessControl
register SassInitializer
register Padrino::Rendering
register Padrino::Mailer
register Padrino::Helpers
enable :sessions
# get '/' do
# "Welcome to me # internet"
# end
use OmniAuth::Builder do
provider :facebook, 'xxxx', 'yyyy'
# provider :facebook, 'app_id', 'app_secret'
end
set :login_page, "/login" # determines the url login occurs
access_control.roles_for :any do |role|
role.protect "/profile"
role.protect "/admin" # here a demo path
end
# now we add a role for users
access_control.roles_for :users do |role|
role.allow "/profile"
end
get :index do
'Hi'
end
get :login do
slim :'index'
end
get :profile do
content_type :text
current_account.to_yaml
end
get :destroy do
set_current_account(nil)
redirect url(:index)
end
get :auth, :map => '/auth/:provider/callback' do
auth = request.env["omniauth.auth"]
# account = Account.find_by_provider_and_uid(auth["provider"], auth["uid"]) ||
# Account.create_with_omniauth(auth)
#
account = User.first( :provider => auth["provider"], :uid => auth["uid"] )
if ! account.nil?
set_current_account(account)
redirect :existing
end
if account.nil?
# Create account
account = User.new
account.uid = auth['uid']
account.name = auth['name']
account.provider = auth['provider']
account.email = auth['user_info']['email'] if auth['user_info']
account.role = 'users'
account.save
end
set_current_account(account)
#redirect "http://" + request.env["HTTP_HOST"] + url(:profile)
redirect :new
end
get :existing do
'existing'
end
get '/session/test' do
session[:test] = 'This is a test'
end
get '/session/print' do
"You saved: #{session[:test]}"
end
end
end
User.rb
class User
include DataMapper::Resource
# property <name>, <type>
property :id, Serial
property :name, String
property :email, String
property :role, String
property :uid, String
property :provider, String
end
What happens >>
List item
I go to [server]/profile ~> redirects to [server]/login
I click on Facebook ~> takes to the page to accept the app ~> redirects back to the app
I go to [server]/profile ~> redirects to [server]/login
I thought that sessions are not working. In the time I was working on my first PHP app, I had similar session based issue. But it turned out to be that it wroks. That is where the [server]/session/test and [server]/session/print came in.
When I login to the Padriono console in Heroku and use User.all I see the entry.
I also see that the user gets authenticated. Some thing has to be with `
I checked the Padrino admin Accounts modal. I think the important parameters would be id and role.
Have I done some thing wrong?
Thanks in advance. Any help is highly appreciated.
After going through the Padrino source code, I noticed that it is expecting the Account class for Padrino Admin authentication.
I was assuming, I could make any class and just use it. But for the moment, I have modified the Account.rb modal and instead of using User ( above ) I used Account.
I write this just as I got it resolved, so the validation section of the modal is commented out.
class Account
include DataMapper::Resource
include DataMapper::Validate
attr_accessor :password, :password_confirmation
# Properties
property :id, Serial
property :name, String
property :surname, String
property :email, String
property :crypted_password, String, :length => 70
property :role, String
property :uid, String
property :display_name, String
property :provider, String
# # Validations
# validates_presence_of :email, :role
# validates_presence_of :password, :if => :password_required
# validates_presence_of :password_confirmation, :if => :password_required
# validates_length_of :password, :min => 4, :max => 40, :if => :password_required
# validates_confirmation_of :password, :if => :password_required
# validates_length_of :email, :min => 3, :max => 100
# validates_uniqueness_of :email, :case_sensitive => false
# validates_format_of :email, :with => :email_address
# validates_format_of :role, :with => /[A-Za-z]/
# Callbacks
before :save, :encrypt_password
##
# This method is for authentication purpose
#
def self.authenticate(email, password)
account = first(:conditions => ["lower(email) = lower(?)", email]) if email.present?
account && account.has_password?(password) ? account : nil
end
##
# This method is used by AuthenticationHelper
#
def self.find_by_id(id)
get(id) rescue nil
end
def has_password?(password)
::BCrypt::Password.new(crypted_password) == password
end
private
def password_required
crypted_password.blank? || password.present?
end
def encrypt_password
self.crypted_password = ::BCrypt::Password.create(password) if password.present?
end
end
Note that just after the role, I added 3 more fields namely uid, display_name and provider.
It seems as though, uid provder and role are what is important for the access control.
The controller / route are the same except for one minor change. That is the Model name.
if account.nil?
# Create account
account = Account.new
Would be interesting to use own modal with Omniauth and Padrino Admin helpers. But for the moment, this is great!
Here's the error I'm getting when I try to run my tests with RSpec:
C:/Ruby193/lib/ruby/gems/1.9.1/gems/activesupport-3.2.11/lib/active_support/infl
ector/methods.rb:230:in `block in constantize': uninitialized constant User (Nam
eError)
I'm trying to run FactoryGirl with RSpec but without Rails. Here are the files that take part in the testing:
user_spec.rb
require 'spec_helper'
module Bluereader
describe User do
describe 'login' do
user = FactoryGirl.build(:user)
end
describe 'logout' do
end
describe 'create_account' do
end
describe 'delete_account' do
end
end
end
spec/spec_helper
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'rspec'
require 'lib/bluereader'
require 'factory_girl'
FactoryGirl.find_definitions
spec/factories.rb
require 'digest/sha1'
FactoryGirl.define do
sequence(:username) { |n| "user-#{n}" }
factory :user do
username
encrypted_password Digest::SHA1.hexdigest('password')
full_name 'John Doe'
logged_in_at Time.now
logged_out_at 0
end
end
At this point I know that the factories.rb file is being loaded (I tried with the moronic print-debugging). When I remove the user = FactoryGirl.build(:user) line from user_spec.rb I get no errors (and the normal RSpec feedback telling me there are no tests, but no errors). If you are interested, here's my model:
require 'digest/sha1'
module Bluereader
class User < ActiveRecord::Base
has_many :categories, :foreign_key => :user_id
has_many :news, :foreign_key => :user_id
has_many :settings, :foreign_key => :user_id
attr_reader :full_name
class << self
def login(username, password)
encrypted_password = Digest::SHA1.hexdigest(password)
if not User.exists?(:username => username, :encrypted_password => encrypted_password)
user_id = User.id_from_username(username)
update(user_id, :logged_in_at => Time.now, :logged_out_at => 0)
end
end
def logout
update(current_user.id, :logged_out_at => Time.now)
end
def validate_account(username, password, full_name)
if username.empty? or password.empty or full_name.empty?
return 'Please fill in all the fields.'
end
if User.exists?(:username => username)
return 'That username is already in use.'
end
unless username =~ /^\w+$/
return 'Username field should contain only letters, numbers and underscores.'
end
''
end
def create_account(username, password, full_name)
encrypted_password = Digest::SHA1.hexdigest(password)
User.create(:username => username,
:encrypted_password => encrypted_password,
:full_name => full_name,
:logged_in_at => Time.now,
:logged_out_at => 0)
end
def delete_account
current_user.destroy
end
private
def id_from_username(username)
user = where(:username => username).first
user.nil? ? 0 : user.id
end
def current_user
where(:logged_out_at => 0).first
end
end
end
end
SOLUTION
The problem was that the class User was in a module, here's the solution:
factory :user, class: Bluereader::User do
You need to require the rails environment in your spec helper file. Add the following to spec/spec_helper.rb:
require File.expand_path("../../config/environment", __FILE__)
Update
Even if you're not using Rails, you'll still need to require the models in your spec helper.
Taken from the bottom of the question
The problem was that the class User was in a module, here's the solution:
factory :user, class: Bluereader::User do
For anyone clumsy like me, you may have FactoryGirl in your code where you meant to have FactoryBot
Shortened version:
Using the omniauth gem for sinatra, I can't get rspec log in to work and keep my session for subsequent requests.
Based on suggestions from http://benprew.posterous.com/testing-sessions-with-sinatra, and turning off sessions, I've isolated the problem to this:
app.send(:set, :sessions, false) # From http://benprew.posterous.com/testing-sessions-with-sinatra
get '/auth/google_oauth2/callback', nil, {"omniauth.auth" => OmniAuth.config.mock_auth[:google_oauth2] }
# last_request.session => {"uid"=>"222222222222222222222", :flash=>{:success=>"Welcome"}}
# last_response.body => ""
follow_redirect!
# last_request.session => {:flash=>{}}
# last_response.body => Html for the homepage, which is what I want
How do I get rspec to follow the redirect and retain the session variables? Is this possible in Sinatra?
From http://benprew.posterous.com/testing-sessions-with-sinatra, it seems like I'd have to send the session variables on each get/post request that I require login for, but this wouldn't work in the case of redirects.
The details:
I'm trying to use the omniauth gem in sinatra with the following setup:
spec_helper.rb
ENV['RACK_ENV'] = 'test'
# Include web.rb file
require_relative '../web'
# Include factories.rb file
require_relative '../test/factories.rb'
require 'rspec'
require 'rack/test'
require 'factory_girl'
require 'ruby-debug'
# Include Rack::Test in all rspec tests
RSpec.configure do |conf|
conf.include Rack::Test::Methods
conf.mock_with :rspec
end
web_spec.rb
describe "Authentication:" do
before do
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:google_oauth2, {
:uid => '222222222222222222222',
:info => {
:email => "someone#example.com",
:name => 'Someone'
}
})
end
describe "Logging in as a new user" do
it "should work" do
get '/auth/google_oauth2/'
last_response.body.should include("Welcome")
end
end
end
When trying to authenticate, I get a <h1>Not Found</h1> response. What am I missing?
On the Integration testing page of the omniauth docs, it mentions adding two environment variables:
before do
request.env["devise.mapping"] = Devise.mappings[:user]
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
end
But seems to be for rails only, as I added
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
to my before block in my spec and I get this error:
Failure/Error: request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
ArgumentError:
wrong number of arguments (0 for 1)
Edit:
Calling get with
get '/auth/google_oauth2/', nil, {"omniauth.auth" => OmniAuth.config.mock_auth[:google_oauth2]}
seems to give me last_request.env["omniauth.auth"] equal to
{"provider"=>"google_oauth2", "uid"=>"222222222222222222222", "info"=>{"email"=>"someone#example.com", "name"=>"Someone"}}
which seems right, but last_response.body still returns
<h1>Not Found</h1>
A partial answer...
The callback url works better, with the added request environment variables:
get '/auth/google_oauth2/callback', nil, {"omniauth.auth" => OmniAuth.config.mock_auth[:google_oauth2]}
follow_redirect!
last_response.body.should include("Welcome")
However, this doesn't work with sessions after the redirect, which is required for my app to know someone is logged in. Updated the question to reflect this.
Using this gist (originating from https://stackoverflow.com/a/3892401/111884) to store session data, I got my tests to store the session, allowing me to pass the session to further requests.
There might be an easier way though.
Setup code:
# Omniauth settings
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:google_oauth2, {
:uid => '222222222222222222222',
:info => {
:email => "someone#example.com",
:name => 'Someone'
}
})
# Based on https://gist.github.com/375973 (from https://stackoverflow.com/a/3892401/111884)
class SessionData
def initialize(cookies)
#cookies = cookies
#data = cookies['rack.session']
if #data
#data = #data.unpack("m*").first
#data = Marshal.load(#data)
else
#data = {}
end
end
def [](key)
#data[key]
end
def []=(key, value)
#data[key] = value
session_data = Marshal.dump(#data)
session_data = [session_data].pack("m*")
#cookies.merge("rack.session=#{Rack::Utils.escape(session_data)}", URI.parse("//example.org//"))
raise "session variable not set" unless #cookies['rack.session'] == session_data
end
end
def login!(session)
get '/auth/google_oauth2/callback', nil, { "omniauth.auth" => OmniAuth.config.mock_auth[:google_oauth2] }
session['uid'] = last_request.session['uid']
# Logged in user should have the same uid as login credentials
session['uid'].should == OmniAuth.config.mock_auth[:google_oauth2]['uid']
end
# Based on Rack::Test::Session::follow_redirect!
def follow_redirect_with_session_login!(session)
unless last_response.redirect?
raise Error.new("Last response was not a redirect. Cannot follow_redirect!")
end
get(last_response["Location"], {}, { "HTTP_REFERER" => last_request.url, "rack.session" => {"uid" => session['uid']} })
end
def get_with_session_login(path)
get path, nil, {"rack.session" => {"uid" => session['uid']}}
end
Sample rspec code:
describe "Authentication:" do
def session
SessionData.new(rack_test_session.instance_variable_get(:#rack_mock_session).cookie_jar)
end
describe "Logging in as a new user" do
it "should create a new account with the user's name" do
login!(session)
last_request.session[:flash][:success].should include("Welcome")
get_with_session_login "/"
follow_redirect_with_session_login!(session)
last_response.body.should include("Someone")
end
end
end
I am writing a small sinatra application that I am integrating with Authlogic (following https://github.com/ehsanul/Sinatra-Authlogic-Template)
Everything works except for when I try to login. I get the following error:
NameError at /login
undefined local variable or method `active' for #<User:0x000001040208f0>
I am including the authlogic gem versus including it as a vendor. So my Sinatra app is not exactly the same as the one on Github.
Any and all inquiries will be MUCH appreciated!! Thanks!
Found out my issue.
Here is the model according to the Github page:
class User < ActiveRecord::Base
acts_as_authentic do |c|
# Bcrypt is recommended
#crypto_provider = Authlogic::CryptoProviders::BCrypt
c.perishable_token_valid_for( 24*60*60 )
c.validates_length_of_password_field_options =
{:on => :update, :minimum => 6, :if => :has_no_credentials?}
c.validates_length_of_password_confirmation_field_options =
{:on => :update, :minimum => 6, :if => :has_no_credentials?}
end
def active?
active
end
def has_no_credentials?
crypted_password.blank? #&& self.openid_identifier.blank?
end
def send_activation_email
Pony.mail(
:to => self.email,
:from => "no-reply#domain.tld",
:subject => "Activate your account",
:body => "You can activate your account at this link: " +
"http://domain.tld/activate/#{self.perishable_token}"
)
end
def send_password_reset_email
Pony.mail(
:to => self.email,
:from => "no-reply#domain.tld",
:subject => "Reset your password",
:body => "We have recieved a request to reset your password. " +
"If you did not send this request, then please ignore this email.\n\n" +
"If you did send the request, you may reset your password using the following link: " +
"http://domain.tld/reset-password/#{self.perishable_token}"
)
end
end
I removed all of the mail methods but my script was failing on the active? method because it was looking for an active column in the users table. Since I am unable to append this column to the table (due to data integrity with another system) I simply told my method to return true
My User.rb
class UserSession < Authlogic::Session::Base
end
class User < ActiveRecord::Base
acts_as_authentic do |c|
end
def active?
return true
end
end
Hope this helps someone!
The simple_client.rb file below works perfectly fine against my emulation cas server; however, the casport.rb file (main file of oa-casport OmniAuth strategy) is not setting or passing the headers / format properly. It needs to be dynamically assigned to the class to allow initializer options to be able to create them, but I'm not sure how else to do it besides how I've attempted to do it here. I was fairly certain I had this working at some point, but I can't see any other explanation for why this wouldn't be working given the simplicity of the client file.
Any help is greatly appreciated on figuring out how to best set the format and headers settings for HTTParty within my Casport class dynamically. As it is it just keeps returning the HTML view for that particular user.
simple_client.rb:
### simple_client.rb - works properly w/ parsed XML response
### The cas.dev project is coming from this Github repo:
### https://github.com/stevenhaddox/oa-casport-server
require 'rubygems'
require 'httparty'
require 'awesome_print'
class Casport
include HTTParty
base_uri 'cas.dev/users'
format :xml
headers 'Accept' => 'application/xml'
def self.find_user(id)
get("/#{id}").parsed_response
end
end
user = Casport.find_user(1)
ap user
casport.rb:
# lib/omniauth/strategies/casport.rb
require 'omniauth/core'
require 'httparty'
require 'redis'
require 'uri'
module OmniAuth
module Strategies
#
# Authentication to CASPORT
#
# #example Basic Usage
#
# use OmniAuth::Strategies::Casport, {
# :setup => true
# }
# #example Full Options Usage
#
# use OmniAuth::Strategies::Casport, {
# :setup => true,
# :cas_server => 'http://cas.slkdemos.com/users/',
# :format => 'xml',
# :format_header => 'application/xml',
# :ssl_ca_file => 'path/to/ca_file.crt',
# :pem_cert => '/path/to/cert.pem',
# :pem_cert_pass => 'keep it secret, keep it safe.'
# }
class Casport
include OmniAuth::Strategy
include HTTParty
def initialize(app, options)
super(app, :casport)
#options = options
#options[:cas_server] ||= 'http://cas.dev/users'
#options[:format] ||= 'xml'
#options[:format_header] ||= 'application/xml'
end
def request_phase
Casport.setup_httparty(#options)
redirect(callback_path)
end
def callback_phase
begin
raise 'We seemed to have misplaced your credentials... O_o' if user.nil?
super
rescue => e
redirect(request_path)
# fail!(:invalid_credentials, e)
end
call_app!
end
def auth_hash
# store user in a local var to avoid new method calls for each attribute
# convert all Java camelCase keys to Ruby snake_case, it just feels right!
user_obj = user.inject({}){|memo, (k,v)| memo[k.gsub(/[A-Z]/){|c| '_'+c.downcase}] = v; memo}
begin
user_obj = user_obj['userinfo']
rescue => e
fail!(:invalid_user, e)
end
OmniAuth::Utils.deep_merge(super, {
'uid' => user_obj['uid'],
'user_info' => {
'name' => user_obj['full_name'],
'email' => user_obj['email']
},
'extra' => {'user_hash' => user_obj}
})
end
# Set HTTParty params that we need to set after initialize is called
# These params come from #options within initialize and include the following:
# :ssl_ca_file - SSL CA File for SSL connections
# :format - 'json', 'xml', 'html', etc. || Defaults to 'xml'
# :format_header - :format Header string || Defaults to 'application/xml'
# :pem_cert - /path/to/a/pem_formatted_certificate.pem for SSL connections
# :pem_cert_pass - plaintext password, not recommended!
def self.setup_httparty(opts)
format opts[:format].to_sym
headers 'Accept' => opts[:format_header]
if opts[:ssl_ca_file]
ssl_ca_file opts[:ssl_ca_file]
if opts[:pem_cert_pass]
pem File.read(opts[:pem_cert]), opts[:pem_cert_pass]
else
pem File.read(opts[:pem_cert])
end
end
end
def user
# Can't get user data without a UID from the application
begin
raise "No UID set in request.env['omniauth.strategy'].options[:uid]" if #options[:uid].nil?
#options[:uid] = #options[:uid].to_s
rescue => e
fail!(:uid_not_found, e)
end
url = URI.escape(#options[:cas_server] + '/' + #options[:uid])
# It appears the headers aren't going through properly to HTTParty...
# The URL + .xml works in the application & the url w/out .xml works in standalone file
# Which means somehow the setup with self.setup_httparty isn't kicking in properly :(
ap Casport.get(url+'.xml').parsed_response
begin
cache = #options[:redis_options].nil? ? Redis.new : Redis.new(#options[:redis_options])
unless #user = (cache.get #options[:uid])
# User is not in the cache
# Retrieving the user data from CASPORT
# {'userinfo' => {{'uid' => UID}, {'fullName' => NAME},...}},
#user = Casport.get(url).parsed_response
cache.set #options[:uid], #user
# CASPORT expiration time for user (24 hours => 1440 seconds)
cache.expire #options[:uid], 1440
end
# If we can't connect to Redis...
rescue Errno::ECONNREFUSED => e
#user ||= Casport.get(url).parsed_response
end
#user = nil if user_empty?
#user
end
# Investigate user_obj to see if it's empty (or anti-pattern data)
def user_empty?
is_empty = false
is_empty = true if #user.nil?
is_empty = true if #user.empty?
# If it isn't empty yet, let's convert it into a Hash object for easy parsing via eval
unless #user.class == Hash
is_empty = true
raise "String returned when a Hash was expected."
end
is_empty == true ? true : nil
end
end
end
end
This was apparently working properly, what I failed to do was to provide the header for Content-Type:
...
def self.setup_httparty(opts)
format opts[:format].to_sym
headers 'Accept' => opts[:format_header]
headers 'Content-Type' => opts[:format_header]
...
Once I added that additional line everything kicked in properly.