I'm upgrading from rails 3 to rails 4 and trying to get digest authentication working based on this example:
http://lightyearsoftware.com/2009/04/testing-http-digest-authentication-in-rails/
It looks like the 'process_with_test' method was removed, so I think I can just override the controller's process method like this:
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 process
def process(name)
credentials = {
:uri => request.url,
:realm => "#{realm}",
:username => "#{user}",
:nonce => ActionController::HttpAuthentication::Digest.nonce(Rails.configuration.secret_key_base),
:opaque => ActionController::HttpAuthentication::Digest.opaque(Rails.configuration.secret_key_base)
}
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Digest.encode_credentials(request.request_method, credentials, "#{password}", false)
real_process(name)
end
)
end
I can see the new method gets called, but I still get 401 access denied errors when I call the controller. I'm not sure I am creating the digest authentication correctly, but I don't know which part is incorrect. Does anyone have tips for debugging this?
I had the same issue. I read through the Rails 4 test cases and built the below solution. Its not perfect by any stretch of the imagination but it works in my test environment. It is a drop-in solution for the original authenticate_with_http_digest helper method.
Gist here:
https://gist.github.com/illoyd/9429839
And for posterity:
# This should go into spec/support/auth_spec_helpers.rb (if you are using RSpec)
module AuthSpecHelpers
##
# Convenience method for setting the Digest Authentication details.
# To use, pass the username and password.
# The method and target are used for the initial request to get the digest auth headers. These will be translated into 'get :index' for example.
# The final 'header' parameter sets the request's authentication headers.
def authenticate_with_http_digest(user, password, method = :get, target = :index, header = 'HTTP_AUTHORIZATION')
#request.env[header] = encode_credentials(username: user, password: password, method: method, target: target)
end
##
# Shamelessly stolen from the Rails 4 test framework.
# See https://github.com/rails/rails/blob/a3b1105ada3da64acfa3843b164b14b734456a50/actionpack/test/controller/http_digest_authentication_test.rb
def encode_credentials(options)
options.reverse_merge!(:nc => "00000001", :cnonce => "0a4f113b", :password_is_ha1 => false)
password = options.delete(:password)
# Perform unauthenticated request to retrieve digest parameters to use on subsequent request
method = options.delete(:method) || 'GET'
target = options.delete(:target) || :index
case method.to_s.upcase
when 'GET'
get target
when 'POST'
post target
end
assert_response :unauthorized
credentials = decode_credentials(#response.headers['WWW-Authenticate'])
credentials.merge!(options)
path_info = #request.env['PATH_INFO'].to_s
uri = options[:uri] || path_info
credentials.merge!(:uri => uri)
#request.env["ORIGINAL_FULLPATH"] = path_info
ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1])
end
##
# Also shamelessly stolen from the Rails 4 test framework.
# See https://github.com/rails/rails/blob/a3b1105ada3da64acfa3843b164b14b734456a50/actionpack/test/controller/http_digest_authentication_test.rb
def decode_credentials(header)
ActionController::HttpAuthentication::Digest.decode_credentials(header)
end
end
# Don't forget to add to rspec's config (spec/spec_helper.rb)
RSpec.configure do |config|
# Include auth digest helper
config.include AuthSpecHelpers, :type => :controller
end
Happy testing.
Related
I'm trying to use iNaturalist's API via Ruby on Rails. I'm new to Ruby and iNaturalist's documentation is pretty sparse. As a first step, I need to figure out how to get authorization from their site.
iNaturalist provides the sample code below. I set up a project with iNaturalist and tried running the sample code in Rails Console with my credentials. #{url} in the following line is replaced with a url that the user is supposed to go to in order to log in to iNat:
puts "Go to #{url}, approve the app, and you should be redirected to your " +
"redirect_uri. Copy and paste the 'code' param here."
I went to the resulting url and logged in:
https://www.inaturalist.org/oauth/authorize?client_id=[my client id]&redirect_uri=https://ruby_on_rails--timrobinson41199691.codeanyapp.com/login/&response_type=code
iNaturalist responds with "The redirect uri included is not valid."
If I leave off &response_type=code, it responds with "The authorization server does not support this response type."
My website is on codeanywhere.com. The url of the main page is "https://ruby_on_rails--timrobinson41199691.codeanyapp.com/". Part of the problem is that I don't understand what kind of page I'm supposed to create for redirect_uri, since I'm still kind of new at this.
require 'rubygems'
require 'rest_client'
require 'json'
site = "https://www.inaturalist.org"
app_id = 'YOUR APP ID'
app_secret = 'YOUR APP SECRET'
redirect_uri = 'YOUR APP REDIRECT URI' # you can set this to some URL you control for testing
# REQUEST AN AUTHORIZATION CODE
# Your web app should redirect the user to this url. They should see a screen
# offering them the choice to authorize your app. If they aggree, they will be
# redirected to your redirect_uri with a "code" parameter
url = "#{site}/oauth/authorize?client_id=#{app_id}&redirect_uri=#{redirect_uri}&response_type=code"
# REQUEST AN AUTH TOKEN
# Once your app has that code parameter, you can exchange it for an access token:
puts "Go to #{url}, approve the app, and you should be redirected to your " +
"redirect_uri. Copy and paste the 'code' param here."
print "Code: "
auth_code = gets.strip
puts
payload = {
:client_id => app_id,
:client_secret => app_secret,
:code => auth_code,
:redirect_uri => redirect_uri,
:grant_type => "authorization_code"
}
puts "POST #{site}/oauth/token, payload: #{payload.inspect}"
puts response = RestClient.post("#{site}/oauth/token", payload)
puts
# response will be a chunk of JSON looking like
# {
# "access_token":"xxx",
# "token_type":"bearer",
# "expires_in":null,
# "refresh_token":null,
# "scope":"write"
# }
# Store the token (access_token) in your web app. You can now use it to make authorized
# requests on behalf of the user, like retrieving profile data:
token = JSON.parse(response)["access_token"]
headers = {"Authorization" => "Bearer #{token}"}
puts "GET /users/edit.json, headers: #{headers.inspect}"
puts RestClient.get("#{site}/users/edit.json", headers)
puts
After the user logs in to iNat, he should be redirected back to my website with the authorization code provided in the data. In routes.rb, my login route is set as:
post '/login', to: 'organisms#login'
I've tried using get, as well.
iNat is returned the error mentioned above and not redirecting back to my site.
OAuth can be a bit daunting at first. And that guide really just shows the equivalent of using cURL to test your API.
In an actual application redirect_uri is whatever endpoint in your application that handles the response when the provider redirects back from authorization.
So lets setup a minimal real rails app.
1. Register your app
Register a new application or edit your existing app.
Use http://localhost:3000/oauth/inaturalist/callback for the callback url (adjust the host as needed).
Keep the window open as you will need the client_id and secret in a moment.
2. Setup your routes
# /config/routes.rb
Rails.application.routes.draw do
# just make sure you have a root path defined.
root to: 'pages#home'
namespace :oauth do
namespace :inaturalist, controller: :callbacks do
# This is just a simple redirect route
get '/', action: :passthru, as: :authorize
# This is the route that handles the actual callback
get 'callback'
end
end
end
You can actually do this without the redirect route and just plant a link to the https://www.inaturalist.org/oauth/authorize... url in your view. But having it isolates your application against the craziness that is OAuth and its how OmniAuth does it.
3. Add your credentials to the Rails app.
In Rails 5 use the encrypted credentials to store your client_id and secret.
Run $ bin/rails credentials:edit from your shell.
inaturalist:
client_id: <from the inaturalist site>
secret: <from the inaturalist site>
In earlier versions use ENV vars instead.
4. Install the oauth2 gem
# Place this in your gemfile outside any groups
gem 'oauth2', '~> 1.4', '>= 1.4.1'
Then run bundle install.
4. Setup the controller
# app/controllers/oauth/inaturalist/callbacks_controller.rb
require 'oauth2'
module Oauth
module Inaturalist
class CallbacksController < ::ActionController::Base
# GET /oauth/inaturalist
def passthru
redirect_to client.auth_code.authorize_url
end
# This endpoint is where the provider redirects the user back to
# after authorization.
# GET /oauth/inaturalist/callback
def callback
# Fetch an auth token from the access code
token = client.auth_code.get_token(params[:code])
# Perform an authenticated request to get the users data
api_response = token.get("/users/edit.json")
#user_data = JSON.parse(api_response.body)
# This is just an example of how you can use the user data from
# the provider
#user = {
uid: #user_data["id"],
nickname: #user_data["nickname"]
}
session[:user_id] = #user[:uid]
session[:token] = token.to_hash
redirect_to root_path, success: "Hello #{#user[:nickname]}"
end
private
# Change this if you are not using Rails 5 credentials.
def client
OAuth2::Client.new(
credentials.fetch(:client_id),
credentials.fetch(:secret),
site: "https://www.inaturalist.org",
redirect_uri: oauth_inaturalist_callback_url
)
end
def credentials
Rails.application.credentials.fetch(:inaturalist)
end
end
end
end
token here is actually a new OAuth2::AccessToken instance that can be called to call endpoints with the fetched credentials.
This example stores the token in the session. You can retrieve it in subsequent requests with:
token = OAuth2::AccessToken.from_hash( session[:token] )
The docs kind of mention trading the oauth access token for an api token for api.inaturalist.org. But the details are kind of sparse.
5 Add a link to sign in:
<%= link_to 'Sign in to iNaturalist.org', oauth_inaturalist_authorize_path %>
I am trying to set up the Zendesk API in my app, I have decided to go with the API that was built by Zendesk
I have set up the initializer object to load the client.
config/initializers/zendesk.rb
require 'zendesk_api'
client = ZendeskAPI::Client.new do |config|
# Mandatory:
config.url = Rails.application.secrets[:zendesk][:url]
# Basic / Token Authentication
config.username = Rails.application.secrets[:zendesk][:username]
config.token = Rails.application.secrets[:zendesk][:token]
# Optional:
# Retry uses middleware to notify the user
# when hitting the rate limit, sleep automatically,
# then retry the request.
config.retry = true
# Logger prints to STDERR by default, to e.g. print to stdout:
require 'logger'
config.logger = Logger.new(STDOUT)
# Changes Faraday adapter
# config.adapter = :patron
# Merged with the default client options hash
# config.client_options = { :ssl => false }
# When getting the error 'hostname does not match the server certificate'
# use the API at https://yoursubdomain.zendesk.com/api/v2
end
This is pretty much copy paste from the site, but I have decided on using the token + username combination.
I then created a service object that I pass a JSON object and have it construct tickets. This service object is called from a controller.
app/services/zendesk_notifier.rb
class ZendeskNotifier
attr_reader :data
def initialize(data)
#data = data
end
def create_ticket
options = {:comment => { :value => data[:reasons] }, :priority => "urgent" }
if for_operations?
options[:subject] = "Ops to get additional info for CC"
options[:requester] = { :email => 'originations#testing1.com' }
elsif school_in_usa_or_canada?
options[:subject] = "SRM to communicate with student"
options[:requester] = { :email => 'srm#testing2.com' }
else
options[:subject] = "SRM to communicate with student"
options[:requester] = { :email => 'srm_row#testing3.com' }
end
ZendeskAPI::Ticket.create!(client, options)
end
private
def for_operations?
data[:delegate] == 1
end
def school_in_usa_or_canada?
data[:campus_country] == "US" || "CA"
end
end
But now I am getting
NameError - undefined local variable or method `client' for #<ZendeskNotifier:0x007fdc7e5882b8>:
app/services/zendesk_notifier.rb:20:in `create_ticket'
app/controllers/review_queue_applications_controller.rb:46:in `post_review'
I thought that the client was the same one defined in my config initializer. Somehow I think this is a different object now. I have tried looking at their documentation for more information but I am lost as to what this is?
If you want to use the client that is defined in the initializer you would need to make it global by changing it to $client. Currently you have it setup as a local variable.
I used a slightly different way of initializing the client, copying from this example rails app using the standard Zendesk API gem:
https://github.com/devarispbrown/zendesk_help_rails/blob/master/app/controllers/application_controller.rb
As danielrsmith noted, the client variable is out of scope. You could instead have an initializer like this:
config/initializers/zendesk_client.rb:
class ZendeskClient < ZendeskAPI::Client
def self.instance
#instance ||= new do |config|
config.url = Rails.application.secrets[:zendesk][:url]
config.username = Rails.application.secrets[:zendesk][:username]
config.token = Rails.application.secrets[:zendesk][:token]
config.retry = true
config.logger = Logger.new(STDOUT)
end
end
end
Then return the client elsewhere by client = ZendeskClient.instance (abridged for brevity):
app/services/zendesk_notifier.rb:
class ZendeskNotifier
attr_reader :data
def initialize(data)
#data = data
#client = ZendeskClient.instance
end
def create_ticket
options = {:comment => { :value => data[:reasons] }, :priority => "urgent" }
...
ZendeskAPI::Ticket.create!(#client, options)
end
...
end
Hope this helps.
I am trying to access Google APIs from my Rails App.
I understand the basic flow for OAuth2.0 for Web Applications.
And I consider myself having a basic understanding of Ruby and Rails.
Yet, I can't make sense of the example Google shows in the "API Client Library for Ruby (Alpha)" Guides.
require 'google/apis/calendar_v3'
require 'google/api_client/client_secrets'
require 'sinatra'
require 'logger'
enable :sessions
def logger; settings.logger end
def calendar; settings.calendar; end
def user_credentials
# Build a per-request oauth credential based on token stored in session
# which allows us to use a shared API client.
#authorization ||= (
auth = settings.authorization.dup
auth.redirect_uri = to('/oauth2callback')
auth.update_token!(session)
auth
)
end
configure do
log_file = File.open('calendar.log', 'a+')
log_file.sync = true
logger = Logger.new(log_file)
logger.level = Logger::DEBUG
Google::Apis::ClientOptions.default.application_name = 'Ruby Calendar sample'
Google::Apis::ClientOptions.default.application_version = '1.0.0'
calendar_api = Google::Apis::CalendarV3::CalendarService.new
client_secrets = Google::APIClient::ClientSecrets.load
authorization = client_secrets.to_authorization
authorization.scope = 'https://www.googleapis.com/auth/calendar'
set :authorization, authorization
set :logger, logger
set :calendar, calendar_api
end
before do
# Ensure user has authorized the app
unless user_credentials.access_token || request.path_info =~ /^\/oauth2/
redirect to('/oauth2authorize')
end
end
after do
# Serialize the access/refresh token to the session and credential store.
session[:access_token] = user_credentials.access_token
session[:refresh_token] = user_credentials.refresh_token
session[:expires_in] = user_credentials.expires_in
session[:issued_at] = user_credentials.issued_at
end
get '/oauth2authorize' do
# Request authorization
redirect user_credentials.authorization_uri.to_s, 303
end
get '/oauth2callback' do
# Exchange token
user_credentials.code = params[:code] if params[:code]
user_credentials.fetch_access_token!
redirect to('/')
end
get '/' do
# Fetch list of events on the user's default calandar
events = calendar.list_events('primary', options: { authorization: user_credentials })
[200, {'Content-Type' => 'application/json'}, events.to_h.to_json]
end
Of course this looks like Ruby, but I never saw some of the constructs used here, and I am not sure where in a Rails app that code would go.
Is it Ruby? If so, how do I use it?
Can somebody tell me the right way to set/use session variables in a ruby on rails application from scratch. I am not able to set/use a session variable in my controller between two pages. I am using CookieStore session type. This is the syntax being used to set and get session variables:
session[:test] = "testing"
#test_str = session[:test]
Let me know in case I am missing on something.
This is how my controller looks like:
class PaymentsController < ApplicationController
# GET /merchant_test
def merchant_test
session[:test] = "testing"
render :layout => false
end
# POST /post_to_mobikwik
def post_to_mobikwik
zr = Mobikwik::Request.new(params)
#mobikwik_data = zr.all_params
#test_str = session[:test]
render :layout => false
end
# POST /z_response
def z_response
zr = Mobikwik::Response.new(request.raw_post)
#checksum_check = zr.valid?
#mobikwik_post = zr.all_params
#statuscode = #mobikwik_post['statuscode']
#statusmessage = #mobikwik_post['statusmessage']
if #statuscode == "0"
#verified = zr.verified?
else
#verified = false
end
render :layout => false
end
Your session cookie settings might be wrong. You should inspect the headers of the response and see what the Set-Cookie header looks like.
It might have a wrong domain or perhaps your cookie is https only and you're on http.
The configuration is usually done in some place like config/initializers/session_store.rb
Myapp::Application.config.session_store :cookie_store, {
:key => '_appname_session_id',
:path => '/',
:domain => nil, # accepted domain, for example '.example.com'
:expire_after => nil, # the session will be expired in X seconds unless active
:secure => false, # if true, cookie is valid only on https
:httponly => true # if true, javascript can't access the cookie
}
Try this in config/initializers/session_store.rb
AppName::Application.config.session_store :cookie_store, key: '_app-name_session'
As a rule of thumb, in rails we create a method
in ApplicationController, called current_user
In this free RailsCast, you see exactly are you need:
I'm trying to get a test signing in using basic authentication. I've tried a few approaches. See code below for a list of failed attempts and code. Is there anything obvious I'm doing wrong. Thanks
class ClientApiTest < ActionController::IntegrationTest
fixtures :all
test "adding an entry" do
# No access to #request
##request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("someone#somemail.com:qwerty123")
# Not sure why this didn't work
#session["warden.user.user.key"] = [User, 1]
# This didn't work either
# url = URI.parse("http://localhost.co.uk/diary/people/1/entries.xml")
# req = Net::HTTP::Post.new(url.path)
# req.basic_auth 'someone#somemail.com', 'qwerty123'
post "/diary/people/1/entries.xml", {:diary_entry => {
:drink_product_id => 4,
:drink_amount_id => 1,
:quantity => 3
},
}
puts response.body
assert_response 200
end
end
It looks like you might be running rails3 -- Rails3 switched over to Rack::test so the syntax is different. You pass in an environment hash to set your request variables like headers.
Try something like:
path = "/diary/people/1/entries.xml"
params = {:diary_entry => {
:drink_product_id => 4,
:drink_amount_id => 1,
:quantity => 3}
env=Hash.new
env["CONTENT_TYPE"] = "application/json"
env["ACCEPT"] = "application/json"
env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("someone#somemail.com:qwerty123")
get(end_point, params, env)
This could work too, but it might be a sinatra only thing:
get '/protected', {}, {'HTTP_AUTHORIZATION' => encode_credentials('go', 'away')}
Sinatra test credit
This works for me in Rails 3
#request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', 'password')
get :index