I'm trying to authenticate against an Oauth2 provider using Rails 4.1. After being redirected to the authorize/deny prompt, and clicking authorize, I'm redirected to my callback url and encounter the following error:
Started GET "/auth/</callback?code=<code>&state=<state>" for 127.0.0.1 at 2014-08-25 12:47:57 +0200
I, [2014-08-25T12:47:57.981471 #12769] INFO -- omniauth: (<provider>) Callback phase initiated.
E, [2014-08-25T12:47:58.697527 #12769] ERROR -- omniauth: (<provider>) Authentication failure! invalid_credentials: OAuth2::Error, invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
{"error":"invalid_client","error_description":"Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method."}
OAuth2::Error (invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
{"error":"invalid_client","error_description":"Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method."}):
oauth2 (0.9.4) lib/oauth2/client.rb:113:in `request'
oauth2 (0.9.4) lib/oauth2/client.rb:138:in `get_token'
oauth2 (0.9.4) lib/oauth2/strategy/auth_code.rb:29:in `get_token'
omniauth-oauth2 (1.1.2) lib/omniauth/strategies/oauth2.rb:93:in `build_access_token'
omniauth-oauth2 (1.1.2) lib/omniauth/strategies/oauth2.rb:75:in `callback_phase'
omniauth (1.2.2) lib/omniauth/strategy.rb:227:in `callback_call'
omniauth (1.2.2) lib/omniauth/strategy.rb:184:in `call!'
omniauth (1.2.2) lib/omniauth/strategy.rb:164:in `call'
omniauth (1.2.2) lib/omniauth/builder.rb:59:in `call'
rack (1.5.2) lib/rack/etag.rb:23:in `call'
rack (1.5.2) lib/rack/conditionalget.rb:25:in `call'
The error is thrown by the oauth2 gem, and it never reaches my /auth/failure endpoint while in development. My routes.rb is as follows
Rails.application.routes.draw do
root 'static_pages#home'
# Auth routes
get '/auth/:provider/callback', to: 'sessions#create'
get '/signin', to: 'sessions#new', as: :signin
get '/signout', to: 'sessions#destroy', as: :signout
get '/auth/failure', to: 'sessions#failure'
end
My sessions controller is just an skeleton, but it never gets there as I said before:
class SessionsController < ApplicationController
def new
redirect_to '/auth/<provider>'
end
def create
redirect_to root_url, notice: 'Signed in'
end
def destroy
redirect_to root_url, notice: 'Signed out'
end
def failure
redirect_to root_url, alert: "Oops: #{params[:messsage].humanize}"
end
end
I'm certain that the ID and secret keys I use work (they work using this tool https://www.runscope.com/oauth2_tool). Im using the following strategy, implemented by the Oauth provider:
module OmniAuth
module Strategies
class <Provider> < OmniAuth::Strategies::OAuth2
# Give your strategy a name.
option :name, '<provider_name>'
option :provider_ignores_state, true
# This is where you pass the options you would pass when
# initializing your consumer from the OAuth gem.
option :client_options, {
site: 'https://<provider>/api/3',
authorize_url: 'https://<provider>/oauth2/authorize',
token_url: 'https://<provider>/oauth2/token'
}
option :authorize_params, {
response_type: 'code'
}
# These are called after authentication has succeeded. If
# possible, you should try to set the UID without making
# additional calls (if the user id is returned with the token
# or as a URI parameter). This may not be possible with all
# providers.
uid{ raw_info['id'] }
info do
{
:name => [ raw_info['first_name'], raw_info['last_name'] ].join(' '),
:email => raw_info['email']
}
end
extra do
{
'raw_info' => raw_info
}
end
def raw_info
#raw_info ||= access_token.get("#{options[:client_options][:site]}/me").parsed
end
end
end
end
I suspect that it is not sending a proper request for an access_token, but I haven't managed to diagnose it yet. Any help would be appreciated.
Related
When I testing the procedure of requesting an access token, which is part of authorize flow, from Doorkeeper gem at localhost side through RSpec with Ruby on Rails, Devise, Grape and Wine_bouncer, RSpec always receives a 401 response from Doorkeeper, whose error description says Invalid-grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
I want to know how to solve this problem. Please help me, thank you.
The followings are my test environments and code:
Ruby 2.3.0
Rails 4.2.5.1
Doorkeeper 3.1.0
Devise 3.5.5
RSpec-core 3.4.2, RSpec-support 3.4.1, RSpec-mocks 3.4.1, RSpec-rails 3.4.1, RSpec-expectations 3.4.0
Wine_bouncer 0.5.1
Doorkeeper configuration at config/initializers/doorkeeper.rb
Doorkeeper.configure do
orm :active_record
resource_owner_authenticator do
session[:user_return_to] = request.fullpath
current_user || redirect_to(new_user_session_url)
end
authorization_code_expires_in 20.minutes
access_token_expires_in 30.days
enable_application_owner :confirmation => false
default_scopes :public, :description => "Access public data."
optional_scopes :write, :description => "Update your data."
optional_scopes :admin, :description => "Do admin things."
access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param
grant_flows %w(authorization_code client_credentials implicit)
skip_authorization do
true
end
end
spec/requests/api/v1/specifiedvegetables_spec.rb
describe SpecifiedVegetables do
describe 'OAuth client requests the grant' do
context 'When a REST client sends a request for getting the grant' do
before(:all) do
post "http://localhost:3000/users/sign_in?user[email]=test%40test123%2Ecom&user[password]=12345678" # Log in the devise
#app = Doorkeeper::Application.new :name => "rspectest-107", :redirect_uri => "https://localhost:3000/api/v1/specified_vegetables/", :scopes => "public"
#app.owner = User.last
#app.save! # Create OAuth client into database.
#authorize_code = String.new # Use later for getting authorize grant code
end
it 'should getting response code 302 for requesting authorization code.' do
query_url = "http://localhost:3000/oauth/authorize"
parameters = { "response_type" => "code", "client_id" => #app.owner.oauth_applications.last.uid, "redirect_uri" => "https://localhost:3000/api/v1/specified_vegetables/", "scope" => "public"}
headers = {'Content-Type' => 'application/x-www-form-urlencoded'}
get query_url, parameters, headers # Send request for getting authorize grant code
expect(response.status).to eq(302)
authorize_code_param = Rack::Utils.parse_query(URI.parse(response.location).query)
#authorize_code << authorize_code_param['code'] # Get the authorize grant code
end
it 'should get response code 302 for requesting access token.' do
query_url = "http://localhost:3000/oauth/token"
parameters = {"grant_type" => "authorization_code", "code" => #authorize_code, "client_id" => #app.owner.oauth_applications.last.uid, "redirect_uri" => "https://localhost:3000/api/v1/specified_vegetables/"}
headers = {'Content-Type' => 'application/x-www-form-urlencoded', "Authorization" => "Basic " + Base64.urlsafe_encode64(#app.owner.oauth_applications.last.uid + ":" + #app.owner.oauth_applications.last.secret, :padding => false)}
post query_url, parameters, headers # Send request for getting access token
expect(response).to eq(200) # **Receive the Error response**
end
after(:all) do
#app.destroy # After running all test cases, destroy this OAuth client application.
end
end
end
end
Error response after running RSpec command at root directory of rails app.
rspec spec/requests/api/v1/specifiedvegetables_spec.rb
expected: 200
got: #, #stream=#, #buf=["{\"error\":\"invalid_grant\",\"error_description\":\"The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.\"}"], #closed=false>, #header={"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff", "Cache-Control"=>"no-store", "Pragma"=>"no-cache", "Content-Type"=>"application/json; charset=utf-8", "WWW-Authenticate"=>"Bearer realm=\"Doorkeeper\", error=\"invalid_grant\", error_description=\"The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.\"", "X-Request-Id"=>"97099b43-3456-4396-b9d9-cf744ec38ea6", "X-Runtime"=>"0.006156", "Content-Length"=>"213"}, #status=401, #sending_file=false, #blank=false, #cv=#, #cond=#>, #committed=false, #sending=false, #sent=false, #content_type=#, #charset="utf-8", #cache_control={:extras=>["no-store"]}, #etag=nil>
20160306 PM10:39 UTC+8 update:
If I try to get access token through Postman software(get it from Chrome store), I can get it as I expect. But trying to get it through rspec, I can't acquire it.
I have used the modified ruby-type code in RSpec, which are from Postman generate function, I still can't get access token successfully. I think this difference is weird.
20160307 PM03:10 Update:
The related file has been put at github.com . I've gotten an access token through Postman software but I still don't understand why I can acquire access token through RSpec although have been tried many other codes to get. It's still unsolved by RSpec.
I've never used Doorkeeper gem, I use Devise Token Auth. I checked the documentation though and found this. You can stub the :doorkeeper_token method to test protected methods.
let(:token) { double :acceptable? => true }
before do
controller.stub(:doorkeeper_token) { token }
# allow(controller).to receive(:doorkeeper_token) {token} # => RSpec 3
end
https://github.com/doorkeeper-gem/doorkeeper/wiki/Testing-protected-controllers
Sorry, I think I have misunderstood about it-statement usage of RSpec test cases after trying many things.
Right meaning of it-statement in RSpec: Every it-statement is independent between each other even if they're under same context- or describe-statement.
So if I combine two it-statements of requesting authorization code and requesting access token together, I will be at halfway towards passing test with RSpec.
The other thing I need to fix is to use right encoding of Authorization at HTTP header for acquiring access token. Change from Base64.urlsafe_encode64 to Base64.strict_encode64, please.
Following are the right code of spec/requests/api/v1/specifiedvegetables_spec.rb:
require 'rails_helper'
describe SpecifiedVegetables do
describe 'OAuth client requests the grant' do
context 'When a REST client sends a request for getting the grant' do
before(:all) do
post "http://localhost:3000/users/sign_in?user[email]=test%40test123%2Ecom&user[password]=12345678"
expect(response.status).to eq(302)
#app = Doorkeeper::Application.new :name => 'rspectest-107', :redirect_uri => 'https://localhost:3000/api/v1/specified_vegetables/', :scopes => 'public'
#app.owner = User.last
#app.save!
#authorize_code = String.new
end
it 'should getting response code 302 for requesting authorization code and access token' do
query_url = "http://localhost:3000/oauth/authorize"
parameters = { "response_type" => "code", "client_id" => #app.owner.oauth_applications.last.uid, "redirect_uri" => "https://localhost:3000/api/v1/specified_vegetables/", "scope" => "public"}
headers = {"content-type" => "application/x-www-form-urlencoded"}
get query_url, parameters, headers
expect(response.status).to eq(302)
authorize_code_param = Rack::Utils.parse_query(URI.parse(response.location).query)
#authorize_code << authorize_code_param['code']
# above are acquiring authorize code
# The following are acquiring access token
query_url = "http://localhost:3000/oauth/token"
parameters = { "grant_type" => "authorization_code", "code" => #authorize_code , "client_id" => #app.owner.oauth_applications.last.uid, "redirect_uri" => "https://localhost:3000/api/v1/specified_vegetables/"}
headers = {"content-type" => "application/x-www-form-urlencoded", "authorization" => "Basic " + Base64.strict_encode64(#app.owner.oauth_applications.last.uid + ":" + #app.owner.oauth_applications.last.secret), "cache-control" => "no-cache"}
post query_url, parameters, headers
expect(response.status).to eq(200) # Here, we get status 200 because the response has access token.
end
after(:all) do
#app.destroy # Destroy the oauth client, but doesn't purge related access token for this rspec request.
end
end
end
end
I have configured Warden in my Rails/Grape API app in config/initializers/warden.rb:
Rails.application.config.middleware.use Warden::Manager do |manager|
manager.default_strategies :password
end
(I'd share the password strategy code, but it really doesn't apply to this question. I have it stored at config/initializers/warden/strategies/password.rb.)
When I run an RSpec rquest spec with invalid login credentials:
describe 'session_tokens', type: :request do
let!(:user) { FactoryGirl.create(:user) }
let(:ip_address) { Faker::Internet.ip_v4_address }
describe 'POST /' do
context 'with invalid password' do
before do
post '/api/v1/session_tokens', email: user.email, password: "#{user.password}.", ip: ip_address
end
it 'is unsuccessful' do
expect(response.code).to eql '401'
end
it 'has an "X-Error-Detail" header' do
expect(response.header['X-Error-Detail']).to eql 'Invalid email or password.'
end
end
end
end
It gives me this error:
Failure/Error: post '/api/v1/session_tokens', email: user.email, password: "#{user.password}.", ip: ip_address
RuntimeError:
No Failure App provided
For the life of me, I cannot get Warden to work correctly with Grape after reviewing examples online (for example, dblock/grape_warden). Most examples are simple and set the Grape app itself as the failure app.
When I do pass a module within my Grape app as the failure_app:
Rails.application.config.middleware.use Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = -> (env) { API::V1::SessionTokens }
end
I get this error, even though I have a post :unauthenticated block in that module:
Failure/Error: post '/api/v1/session_tokens', email: user.email, password: "#{user.password}.", ip: ip_address
NoMethodError:
undefined method `unauthenticated' for API::V1::SessionTokens:Class
The same happens when I move the unauthenticated definition to the root of my Grape app.
The solution was to configure Warden within the Grape app itself instead of within a Rails initializer.
I removed config/initializers/warden.rb and put its contents within my Grape app as such:
module API
module V1
class Base < Grape::API
mount API::V1::SessionTokens
use Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = API::V1::SessionTokens
end
end
end
end
Works perfectly now!
Kudos to Jean Bahnik for this file on GitHub in particular. I found this beautiful piece of code after almost giving up.
I created a session_controller to handle token authentication, but I got this error:
Started POST "/users/sign_in" for 127.0.0.1 at 2013-04-29 20:29:44 +0200
Processing by SessionsController#create as */*
Parameters: {"user_login"=>{"login"=>"admin#example.com.", "password"=>"[FILTERED]"}}
Completed 500 Internal Server Error in 1ms
ArgumentError (wrong number of arguments (2 for 1)):
activerecord (3.2.13) lib/active_record/attribute_methods.rb:24:in `[]'
.../bundler/gems/devise-dceb788c6b53/lib/devise.rb:436:in `block (2 levels) in configure_warden!'
warden (1.2.1) lib/warden/session_serializer.rb:35:in `fetch'
warden (1.2.1) lib/warden/proxy.rb:212:in `user'
warden (1.2.1) lib/warden/proxy.rb:318:in `_perform_authentication'
warden (1.2.1) lib/warden/proxy.rb:104:in `authenticate'
warden (1.2.1) lib/warden/proxy.rb:114:in `authenticate?'
.../bundler/gems/devise-dceb788c6b53/app/controllers/devise_controller.rb:124:in `require_no_authentication'
My controller looks like:
class SessionsController < DeviseController
before_filter :ensure_params_exist
respond_to :json
def create
build_resource
resource = User.find_for_database_authentication(:email => params[:user_login][:login])
return invalid_login_attempt unless resource
if resource.valid_password?(params[:user_login][:password])
sign_in("user", resource)
render :json => {:success => true, :auth_token => resource.authentication_token, :email => resource.email}
return
end
invalid_login_attempt
end
And my route file:
devise_for :users, :controllers => {:sessions => "sessions"}
Whats wrong?
Your class declaration does not match Devise namespace:
class SessionsController < Devise::SessionsController
Stage:
I have a Rails app, it should authenticate users using their twitter account, so I use Omniauth-Twitter gem.
I have configured one app in dev.twitter.com enabling Sign in with twitter flag and getting consumer_key and consumer_secret.
I was following this guide https://github.com/arunagw/omniauth-twitter#readme
In Rails app, I have configured some files:
config/initializers/omniauth.rb:
Rails.application.config.middleware.use OmniAuth::Builder do
# Twitter consumer data
CONSUMER_KEY='xxxxxxxxxxxxxxxxx'
CONSUMER_SECRET='yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
provider :twitter, CONSUMER_KEY, CONSUMER_SECRET
end
sessions_controller.rb looks like:
class SessionsController < ApplicationController
def create
raise request.env["omniauth.auth"].to_yaml
end
end
applications.html.erb layout:
<div id="user_nav">
<%= link_to "Sign in with Twitter", "/auth/twitter" %>
</div>
So I except that when I clic on Sign in with Twitter link, I raises an exception showing me authentication data or requesting me Twitter autorization.
Problem:
Instead of this, it returns back this:
OAuth::Unauthorized (401 Unauthorized):
oauth (0.4.7) lib/oauth/consumer.rb:216:in `token_request'
oauth (0.4.7) lib/oauth/consumer.rb:136:in `get_request_token'
omniauth-oauth (1.0.1) lib/omniauth/strategies/oauth.rb:29:in `request_phase'
omniauth-twitter (0.0.15) lib/omniauth/strategies/twitter.rb:63:in `request_phase'
omniauth (1.1.3) lib/omniauth/strategy.rb:207:in `request_call'
omniauth (1.1.3) lib/omniauth/strategy.rb:174:in `call!'
omniauth (1.1.3) lib/omniauth/strategy.rb:157:in `call'
omniauth (1.1.3) lib/omniauth/builder.rb:48:in `call'
What is wrong here?
In my project, I have two type of users: job seekers and hiring managers. Job seekers don't have a model, they are just able to apply for jobs using the data received from from third-party providers while authenticating thru Omniauth. Hiring managers' info is stored in devise User model. Hiring managers also must be able to sign in with their company's Google email account.
So, first I built job seekers' authentication using Omniauth 1.0.0, Rails 3.1.3:
omniauth.rb
require 'omniauth-openid'
require 'openid/store/filesystem'
Rails.application.config.middleware.use OmniAuth::Builder do
provider :openid, :store => OpenID::Store::Filesystem.new('./tmp'), :name => 'google', :identifier => 'https://www.google.com/accounts/o8/id'
provider :facebook, "xxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
{:scope => 'email, offline_access, publish_stream', :client_options => {:ssl => {:ca_file => '/usr/lib/ssl/certs/ca-certificates.crt'}}}
provider :twitter, "xxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
provider :linkedin, "xxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxx"
end
in routes.rb:
match '/auth/:provider/callback', :to => 'sessions#authenticate_jobseeker'
match '/auth/failure', :to => 'sessions#failure'
in sessions_controller.rb
def authenticate_jobseeker
session[:jobseeker] = request.env['omniauth.auth']
if valid_job_seeker?
redirect_to new_job_application_path(...)
else
redirect_to request.env['omniauth.origin'] || root_path, alert: "Authentication failure"
end
end
Up to this point everything worked fine. However, when I started implementing Google sign on for User model, and added :omniauthable to it, my job seeker authentication broke. I am using Devise 1.5.2:
user.rb
class User < ActiveRecord::Base
#...
devise :database_authenticatable, :registerable,
... :lockable, :omniauthable
#...
end
in devise.rb:
config.omniauth :open_id, :store => OpenID::Store::Filesystem.new('./tmp'), :name => 'google', :identifier => 'https://www.google.com/accounts/o8/id', :require => 'omniauth-openid'
in routes.rb:
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" } do
get '/users/auth/:provider' => 'users/omniauth_callbacks#passthru'
end
At this point, Users' authentication worked, but job seekers' did not. After searching for a while, the issue was fixed by adding :path_prefix => "/auth" to every provider in omniauth.rb.
The only problem now, is when job seeker does not allow access to its data (i.e. presses "Don't Allow" and comes back to the application), I get following RuntimeError for every provider:
Could not find a valid mapping for path "/auth/twitter/callback"
Parameters:
{"denied"=>"mKjVfMRwRAN12ZxQ9cxCoD4rYSLJIRLnEqgiI"}
top of the trace:
devise (1.5.2) lib/devise/mapping.rb:48:in `find_by_path!'
devise (1.5.2) lib/devise/omniauth.rb:17:in `block in <top (required)>'
omniauth (1.0.0) lib/omniauth/strategy.rb:418:in `call'
omniauth (1.0.0) lib/omniauth/strategy.rb:418:in `fail!'
omniauth-oauth (1.0.0) lib/omniauth/strategies/oauth.rb:63:in `rescue in callback_phase'
omniauth-oauth (1.0.0) lib/omniauth/strategies/oauth.rb:45:in `callback_phase'
omniauth (1.0.0) lib/omniauth/strategy.rb:200:in `callback_call'
omniauth (1.0.0) lib/omniauth/strategy.rb:166:in `call!'
omniauth (1.0.0) lib/omniauth/strategy.rb:148:in `call'
omniauth (1.0.0) lib/omniauth/strategy.rb:168:in `call!'
omniauth (1.0.0) lib/omniauth/strategy.rb:148:in `call'
omniauth (1.0.0) lib/omniauth/strategy.rb:168:in `call!'
omniauth (1.0.0) lib/omniauth/strategy.rb:148:in `call'
omniauth (1.0.0) lib/omniauth/builder.rb:30:in `call'
I've been trying to solve it for a while now. Any help is greatly appreciated. Let me know, if I can provide additional info.
Answering my own question. So, final decision was to go with pure Omniauth implementation. I removed :omniauthable from User model, removed config.omniauth... from devise.rb, removed :omniauth_callbacks devise routes from routes.rb.
So, all users (no matter what role) would use ame callback routes and hit sessions_controller#authenticate_jobseeker action (should consider renaming the action?):
def authenticate_jobseeker
auth_hash = request.env['omniauth.auth']
unless auth_hash.present?
redirect_to request.env['omniauth.origin'] || root_path, alert: "Sorry, we were not able to authenticate you" and return
end
#user = User.find_from_oauth(auth_hash)
if #user.present?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
sign_in_and_redirect #user, :event => :authentication and return
else
session[:jobseeker] = auth_hash["info"]
if valid_job_seeker?
redirect_to new_job_application_path(...)
end
end
end
and User.find_from_oauth:
def self.find_from_oauth(auth_hash)
if auth_hash
user = User.where(:email => auth_hash["info"]["email"]).first
end
user
end
This implementation satisfied all of the requirements.
Are you using omniauth["user_info"] in your models somewhere? In my case, I was accessing
omniauth["user_info"]["email"]
and that would crash and I would get the same error, being caught by devise.
In my app as well, we use omniauth directly (for businesses) as well as use device+facebook for user logins.
Havent yet figured out to not get failure caught by devise though. Devise registers it's own failure app. Will update when i figure it out.
Update: I'm sorry it seems I misread part of your question. You can see a clear failure to authorize from the remote webapp which seems to stuff up and not a masked exception from the code (as was in my case).