I have a RESTful Rails application with a resource called "Foo". I'm trying to use REST Client to do a put:
resource = RestClient::Resource.new 'http://localhost:3000/foos/1', :user => 'me', :password => 'secret'
resource.put :name => 'somethingwitty', :content_type => 'application/xml'
But my app raises:
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
/usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/request_forgery_protection.rb:86:in `verify_authenticity_token'
It seems like my app isn't getting the message that this is an XML request and that the AuthenticityToken should be ignored. Maybe I'm not using REST Client correctly. Any ideas on why I'm getting the exception?
Try putting an :only => [:update, :delete, :create] on the protect_from_forgery line in your application controller.
More info: http://ryandaigle.com/articles/2007/9/24/what-s-new-in-edge-rails-better-cross-site-request-forging-prevention
Use something like:
resource.put '<foo><name>somethingwitty</name></foo>', :content_type => 'application/xml'
I think you need to make two changes;
(a) Use the rails routing to tag this as an XML request
(b) Use HTTP Basic Authentication to authenticate the request.
This means changing your URL above to include the username, password like this
me:secret#localhost:3000/foos/1.xml
also note .xml bit
I guess that somewhere on your server-side you have code that authenticates in-bound requests via a before filter. This needs to work something like this ...
#
# If you haven't authenticated already then you are either
# reqirected to the logon screen (for HTML formats) or
# the browser prompts you. You are always allowed to pass
# the username/password in the URL
#
def login_required
#current_user = valid_session?
unless #current_user
if params["format"]
#
# If you specify a format you must authenticate now
#
do_basic_authentication
else
display_logon_screen
end
end
end
#
# Ask Rails for the login and password then authenticate as if this
# were a new login.
#
def do_basic_authentication
user = authenticate_with_http_basic do |login, password|
User.authenticate(login, password)
end
if user
current_user(#current_user = user)
else
request_http_basic_authentication
end
end
That's from our own app and is triggered by a before_filter in ApplicationController.
Also, I don't think you need the :content_type => 'application/xml'. What I normally do is just call post or put directly like this ..
response = RestClient.post URI.encode(url), :record => args
where the url contains the basic authentication and the ".xml"
Happy coding
Chris
Since your application is a Rails app, it might be easier to use ActiveResource for the client.
Something like:
require 'active_resource'
class Foo < ActiveResource::Base
self.site = 'http://localhost:3000/'
end
foo = Foo.new(:name => 'somethingwitty')
foo.save
You can read up on how to do the authentication on the rdoc site.
Related
I currently have a Rails application that is connected to an existing SQL database. I am using Devise for my user management, however the pre-existing User table in the database uses a very customized password encryption method.
There is a web service I can connect to that passes a JSON object with the login information to authenticate whether it is valid or not, and I have to manage my own session and everything after that.
I attempted to follow "Railscast #250", and combine it with Devise and some Stack Overflow searches, but things are not going very well.
This is what I have now, but it isn't doing anything, and I just don't feel like I am on the right track with this.
class SessionsController < Devise::SessionsController
def new
super
end
def create
post_params = {
"RuntimeEnvironment" => 1,
"Email" => params[:session][:email],
"Password" => params[:session][:password]
}.to_json
user_params = RestClient.post 'http://some.ip/WebServices', post_params, :content_type => "json"
user = User.authenticate(user_params)
if user
session[:user_id] = user.user_id
redirect_to root_path
else
flash.now.alert = "Invalid Username or Password"
render "new"
end
end
end
This is the JSON Object returned if there is a successful login:
{"Success":true,"ErrorMessage":"","ResponseString":"","LoginResultData":{"FailMessage":"","ResultCode":0,"User":{"AccountCompleteFlag":1,"CreationDtime":"\/Date(1430848539000-0400)\/","DeleteFlag":0,"Email":"john#doe.com","FailedPasswordCount":1,"HistoricalFlag":0,"IsDirty":false,"IsAdminFlag":0,"IsSiteAdminFlag":0,"LastLoginDtime":"\/Date(1447789258000-0500)\/","NameFirst":"Ttest","NameLast":"test","Password":"TRQt3d2Z7caDsSKL0ARVRd8nInks+pIyTSqp3BLxUgg=","PasswordLockDtime":"\/Date(-62135578800000-0500)\/","PasswordLockFlag":0,"PasswordResetCode":"","PasswordResetStatus":0,"Phone":"1-X-5555555555-","RegistrationSource":"Registration","UserId":100029,"UserType":1,"PhoneInfo":{"AreaCode":"555","CountryCode":"X","Extension":"","FirstThree":"555","InternationalPhoneNumber":"","IsDirty":false,"IsInternational":false,"LastFour":"5555"}}}}
And what is returned for a failed one:
{"Success":true,"ErrorMessage":"","ResponseString":"","LoginResultData":{"FailMessage":"Invalid email address","ResultCode":1,"User":null}}
Is there a way where I can use Devise's session management while connecting to the API?
You can still authenticate through Devise using the email and password that the user provided. The RestClient would just be like a double check: just make sure that there are no routes that the user can authenticate through besides going through the RestClient. You can check this by doing rake routes.
For checking whether the result code was valid, you can do some JSON parsing as follows:
authentication_response = RestClient.post 'http://some.ip/WebServices', post_params, :content_type => "json"
json_authentication_response = JSON.parse(authentication_response)
result_code = json_authentication_response["LoginResultData"]["ResultCode"]
if result_code == 0
# Authenticate
else
# Don't authenticate
end
How would I write a method to be used in rspec testing to access pages that require a username and password for HTTP Digest Authentication. For example, this test...
it "edit" do
http_login
post :edit, id: #post
assigns[:post].should eq(#post)
end
needs http_login method to be something like this...
def http_login
user = {"username" =>
Digest::MD5.hexdigest(["username","Application","password"].join(":"))}
request.env['HTTP_AUTHORIZATION'] =
ActionController::HttpAuthentication::Digest.encode_credentials(?,?,?,?)
end
My question is what do I put in the four arguments for the encode credentials. The arguments are going to be http_method, credentials, password, password_is_ha1 but I'm unsure how to write http_method and credentials to implement in the tests.
Solution here: https://gist.github.com/1282275
recopied here for posterity
# Adds support for http digest authentication in Rails 3
# Inspired by: http://lightyearsoftware.com/2009/04/testing-http-digest-authentication-in-rails/
# Place this code in test/test_helper.rb
# In your test, call authenticate_with_http_digest prior to calling get, post, put or delete
# Tested with Rails 3.0.7
class ActionController::TestCase
require 'digest/md5'
def authenticate_with_http_digest(user = API_USERNAME, password = API_PASSWORD, realm = API_REALM)
ActionController::Base.class_eval { include ActionController::Testing }
#controller.instance_eval %Q(
alias real_process_with_new_base_test process_with_new_base_test
def process_with_new_base_test(request, response)
credentials = {
:uri => request.url,
:realm => "#{realm}",
:username => "#{user}",
:nonce => ActionController::HttpAuthentication::Digest.nonce(request.env['action_dispatch.secret_token']),
:opaque => ActionController::HttpAuthentication::Digest.opaque(request.env['action_dispatch.secret_token'])
}
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Digest.encode_credentials(request.request_method, credentials, "#{password}", false)
real_process_with_new_base_test(request, response)
end
)
end
end
Here's a solution for RSpec 3.1 and Rails 4 HTTP Digest Auth testing: https://gist.github.com/murbanski/6b971a3edc91b562acaf
I'm having trouble using omniauth & twitter gem to generate tweets.
I have been using this tutorial http://blog.assimov.net/post/2358661274/twitter-integration-with-omniauth-and-devise-on-rails-3
and I can log in using twitter, generate authentications but I cannot update tweets.
current_user.twitter.update("My Rails 3 App with Omniauth, Devise and Twitter")
This line complains about the method update.
What confuses me about the tutorial above is that at the beginning they generate a migration to store the secret in authentications, but at no point later in the tutorial is any mention of changing any code to write the secret to the database.
My understanding is that the secret is obtained from the omniauth hash that is stored in the session cookie. What am I missing here?
def hash_from_omniauth(omniauth)
{
:provider => omniauth['provider'],
:uid => omniauth['uid'],
:token => (omniauth['credentials']['token'] rescue nil),
:secret => (omniauth['credentials']['secret'] rescue nil)
}
end
So everything seems to be working apart from creating the Twitter Client hence not having the update method available?
current_user.twitter.update("first tweet")
The twitter method here should be creating the Twitter Client
def twitter
debugger
unless #twitter_user
provider = self.authentications.find_by_provider('twitter')
#twitter_user = Twitter::Client.new(:oauth_token => provider.token, :oauth_token_secret => provider.secret )rescue nil
end
#twitter_user
end
I'm sorry I'm not great at explaining the problem. Any help greatly appreciated.
Thanks
L
If you only need to post to twitter, it might just be easier to forgo the twitter gem completely. The consumer / access token generation requires the app id and secret as well as the user's access and secret tokens. I found this to be easy enough to implement that I thought the overhead of the twitter gem wasn't necessary.
module User::Social
def self.included(base)
base.instance_eval do
include Rails.application.routes.url_helpers
end
end
def promote_activity(type, profile)
url = short_profile_url(profile, :host => Conf.domain)
tw_client.request(:post, "http://api.twitter.com/1/statuses/update.json", :status => I18n.translate("tweets.#{type}", :profile => profile.to_s, :url => url))
end
def tw_client
#tw_client ||= begin
consumer = OAuth::Consumer.new(Conf.tw_app_id, Conf.tw_secret, :site => 'http://api.twitter.com')
OAuth::AccessToken.from_hash(consumer, {:oauth_token => self.access_token, :oauth_token_secret => self.secret_token})
end
end
end
class User < AR::Base
include User::Social
end
I am running Ruby on Rails 3 and I would like to set up my routes to show additional information in the URL using namespaces.
In the routes.rb file I have:
namespace "users" do
resources :account
end
So, the URL to show an account page is:
http://<site_name>/users/accounts/1
I would like to rewrite/redirect that URL as/to
http://<site_name>/user/1/Test_Username
where "Test_username" is the username of the user. Also, I would like to redirect all URLs like
# "Not_real_Test_username" is a bad entered username of the user.
http://<site_name>/users/accounts/1/Not_real_Test_username
to the above.
At this time I solved part of my issuelike this:
scope :module => "users" do
match 'user/:id' => "accounts#show"
end
My apologies for not answering your question (#zetetic has done that well enough), but the best practice here is to stay within the RESTful-style Rails URL scheme except for rare exceptions. The way most people make pretty URLs in this way is to use a hyphen, e.g.:
/accounts/1-username
This does not require any routing changes. Simply implement:
class Account < ActiveRecord::Base
def to_param
"#{self.id}-#{self.username}"
end
end
And handle the extra string data in your finds by calling to_i.
class AccountController < ApplicationController
def show
#account = Account.find(params[:id].to_i)
end
end
When you do link_to 'Your Account', account_path(#account), Rails will automatically produce the pretty URL.
It's probably best to do this in the controller, since you need to retrieve the account to get the username:
#account = Account.find(params[:id])
if #account && #account.username
redirect_to("/user/#{#account.id}/#{#account.username}")
return
end
As to the second issue, you can capture the remaining parameter by defining it in the route:
get "/users/accounts/:id(/:other)" => "users/accounts#show"
This maps like so:
/users/accounts/1/something # => {:id => "1", :other => "something"}
/users/accounts/1 # => {:id => "1"}
And you can simply ignore the :other key in the controller.
Rails AuthenticityToken automatically protects POST/PUT/DELETE requests from CSRF attacks. But I have another use case in mind.
I am showing a video on my site that I don't want to be embeddable on other sites. How this works is that my flash player sends a request for a signed URL from my CDN that expires in a few seconds. Up until now a user had to be logged in to watch videos, so that was the authentication. However now I want any visitor to the site to be able to watch the video without allowing the signed URL to be requested from another site (such as if they embedded our player on their site).
My first thought went to AuthenticityToken since it seems to have these exact semantics... all I need to do is plug it into a GET request. Any ideas?
Rails, opinionated as it is believes that all GET requests should be idempotent. This means Rails of course does not check authenticity tokens for GET requests, even verified_request? gives every GET a pass.
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
end
So we have to write our own logic. We can use form_authenticity token. All this does is create a random string and cache it in the session:
def form_authenticity_token
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
end
We can therefore make a before filter that tests the equality of a url parameter to the session token. Thereby ensuring that only bonafide visitors can view videos.
Controller:
class CDNController < ActionController::Base
# You probably only want to verify the show action
before_filter :verify_request, :only => 'show'
# Regular controller actions…
protected
def verify_request
# Correct HTTP response code is 403 forbidden, not 404 not found.
render(:status => 403) unless form_authenticity_token == params[:token]
end
end
The view:
<%= video_path(:token => form_authenticity_token) %>
To plug the authenticity token in your url:
<%= video_path(:token => form_authenticity_token) %>
In your CDN's controller, you could check if the authenticity token is correct with a before_filter:
def verify_token
render_404 unless form_authenticity_token == params[:token]
end
def render_404
render :file => "#{RAILS_ROOT}/public/404.html", :status => 404
end