Usually, when a Github app installation is initialized from my application, the omniauth gem initiates the request phase, sets the state param and the cookie and redirects users to the installation URL. After installation, the request is sent back to the backend where omniauth checks that the state param is present and that it's equal to the one in the cookie to prevent a CSRF attack. When the GitHub app is installed from the Marketplace, the user is redirected to the installation URL without the state param. When the backend receives the response, the omniauth gem raises the CSRF error. Is there a way to prevent it without resorting to setting the ignore_provider_state: true option?
This my Github strategy:
module OmniAuth
module Strategies
class GitHubApp < OmniAuth::Strategies::GitHub
option :client_options, {
site: 'https://api.github.com',
authorize_url: "https://github.com/apps/#{ENV['GITHUB_APP_SLUG']}/installations/new",
token_url: 'https://github.com/login/oauth/access_token'
}
option :redirect_url, "#{Settings.app.host}/auth/github/callback"
info do
{
'username' => raw_info['login'],
}
end
def callback_url
options.redirect_url || (full_host + callback_path)
end
end
end
end
Related
I want to achieve the following login workflow:
user clicks on login button
user gets redirected to our authentication platform
user submits the login credentials and then gets redirected back to our
website via a pre-set callback URL
the OmniAuth strategy must decode the response (using our SDK) and
save the result in the omniauth.auth hash
Is this process easily achievable using a OmniAuth Strategy? It's not very clear to me from the documentation, and the majority of already built strategies seem to be using the OAuth workflow.
Apparently this is quite easy to do.
The OmniAuth strategy
module OmniAuth
module Strategies
class Service
include OmniAuth::Strategy
def request_phase
redirect AUTHENTICATION_URL
end
uid { #user_details.user_id }
def extra
#user_details # Return a hash with user data
end
def callback_phase
# Configure Service SDK
#user_details = Service.user_data # Make SDK call to get user details
super
end
end
end
end
The App
1) Add a login button with the authentication URL:
<%= link_to 'Login', 'auth/service' %>
2) Add a callback route
get '/auth/service/callback', to: 'sessions#create'
3) Handle the callback response in the controller
class SessionsController < ApplicationController
def create
#user = User.find_or_create_by(service_id: auth_hash.uid)
# Handle #user
end
end
I've implemented my own classes for handling authorization via OAuth to GitHub using Faraday in Ruby. I've verified that under the following conditions:
Not logged into GitHub
No token exists for the app
that a request for authorization via GET to "/login/oauth/authorize" with a random state variable:
Redirects to the GitHub login page
Redirects to the Authorize Application page after login
Executes callback to my app with temporary code after authorizing
Responds with access_token when I POST to "/login/oauth/access_token" with temporary code
The problem I have is when I alter the first condition, I'm not already logged into GitHub. The same GET request is sent to GitHub, I see the correct URL with the right parameters. I then see what appears to be the correct redirect by GitHub with a return_to parameter, but it quickly just redirects again back to the GitHub home page.
I'm hoping it's something easy like forgetting a header parameter or something, and someone might spot the problem right away. Anyway, any help is appreciated...
Code to setup Faraday connection:
def connection
#connection ||= Faraday.new(url: 'https://github.com') do |faraday|
faraday.request :url_encoded
faraday.response :logger
faraday.adapter Faraday.default_adapter
end
end
Code to send authorization request:
def request_authorization(client_id, redirect_uri, redirect_id, scope, expected_state)
response = connection.get '/login/oauth/authorize', {
client_id: client_id,
redirect_uri: "#{redirect_uri}?id=#{redirect_id}",
scope: scope,
state: expected_state
}
if response.status == 302
response.headers[:location]
else
nil
end
end
I didn't show the code, but my controller does a redirect to the URL reply from request_authorization(). Again, I definitely see the redirect from my controller in both cases, but the second case seems to encounter something GitHub didn't like in the redirected request. I assume it then redirects to the home page and never replies to my app because of this unknown problem in my original request.
Thanks,
David
Ivan from GitHub was a great help in finding the answer to my question. I had assumed the problem was some detail with using Faraday or OAuth, but it turns out the problem was a basic assumption that proved wrong. Hopefully this will help others that run into a similar misunderstanding.
I had assumed that a user of my app that wanted to connect to GitHub (or another OAuth service) would issue something like a "connect" request to my app. My app would then generate the OAuth authorization request to GitHub, handle any redirects, and eventually wind up presenting the Authorize App page to the user for acceptance.
Turns out I just needed to make the "connect" request actually a link to directly make the authorization request to GitHub. My app then only has to worry about handling the callback, which it already did. Easier and works in all cases now.
Turns out the wrong approach worked when not logged in due to it being the simple case. It failed when logged in because I wasn't handling session state that normally the browser would provide.
A more careful read of the OAuth RFC cleared up my confusion about where requests and responses are handled for the User Agent and for the Client.
I'm building an API with Sinatra (using Angular for the client side and want others to have access to API) and have it also be an OAuth provider. I am wondering what the best route to take (work off existing gems or roll own solution off Warden or something).
Have used devise and doorkeeper for authentication and oauth before with Rails, wondering what best solution for Sinatra is.
Ideally I don't want the views or be able to extend/mod the actions of an existing solution, as I'm interacting with it purely as an API.
I just recently did the same thing using the following answer from S/O
What is a very simple authentication scheme for Sinatra/Rack
It implies a user model, but instead of using that, I just set a user and admin password in my config file. Then I had a login form that just took a password. When the user enters that password, I checked it against the one in settings and set the session['user'] to :admin or :user according to whichever it matched (or nil if none). Then on each of my routes, I called auth: :user or auth: :admin accordingly.
APIs normally accept your login request and send you an authentication token which you need to pass back in each call. This is very similar to cookie based sessions where your browser automatically passes back the cookie which is acquired on initial website visit.
From what I've seen in Sinatra's docs, you could make a session-based authentication system like this:
enable :session
disable :show_exceptions
use Rack::Session::Pool,
key: 'session_id'
post '/login' do
user = User.login_success(params)
halt 401 if user.nil?
session[:user] = user
200
end
get '/fun' do
user = session[:user]
halt 401 if user.nil?
halt 403 if !user.has_permission_for '/fun'
"fun was had"
end
Now all you need to do in your client is to pass back the cookie token returned in response to initial visit when requesting an API function. This can be done with any web client library that supports cookie stores (such as libcurl) or by inserting the session cookie into the request header manually. Rack::Minitest functionality also supports cookies, so you can test your API with minitest.
See Sinatra API Authentication.
Quick summary:
Sinatra has no built-in auth.
It's best to build auth yourself (see the link).
There are gems available, but you probably won't need them for something as simple as an API.
Alright, I'm a huge RubyCAS noob, and this is driving me crazy.
I have installed the rubycas-client gem, and have followed along with the official Sinatra setup at this repo. The way this setup is done, every time I request a page, I receive a one-time ticket that's appended to the URL as a query like so:
http://localhost:9393/?ticket=ST-1373928850... etc.
If I refresh the page, I get a Sinatra error saying the ticket has already been used up!
I've two questions, then.
Is the ticket-per-reload standard behavior?
How do I save my CAS login for a session and still retain single sign-out?
What I've done:
I have gone and tried to implement :sessions in Sinatra, but this causes single-sign-out to fail.
I have gone and done my best to follow the steps in the rubycas-client GitHub Repo (replacing ActiveRecord session storage with Sinatra's :session helper).
The RubyCAS documentation for Sinatra is fairly poor, so I'm looking for a definitive answer to this.
It is doing the correct thing when you try and reload the page with the same ticket. That ticket has already been validated. When you get the validation response you need to then set your own applications cookie or other session option.
I usually add a method that will add a session attribute to the user's cookie like:
session["cas"]["username"] = <user from cas validation response>
Then in future requests the Sinatra application can protect whatever routes you want with a helper method like:
cas = RestClient::Resource.new "#{cas_url}/login", :timeout => 5
checked = cas.get
return true if checked.code == 200
In my configure block for Sinatra I do this:
use Rack::Session::Cookie, :key => "example.com",:secret => "veryrandomhex"
I hope this helps, have any questions let me know.
UPDATE BELOW
While discussing this problem, we've uncovered that RubyCas says to not use a regular cookie session for your ruby application in production, while using CAS. What you'll want to do is:
A. Make sure your cookie expires at the same time or sooner than the CAS cookie
And/Or
B. Make sure your cookie is per browser session, then revalidate the CAS user on next browser session.
For Rack cookie you would specify this extra config for when the cookie is set to expire: :expire_after => 1440, (where 1440 is in minutes)
In case of the ruby CAS there are two kinds of session :
(1). The application session.
(2). The Single sign on (SSO) session.
you can use sinatra-session gem for managing the application session and just use session_end! helper method to destroy the application session. For destroying the SSO session unset the session[:cas_ticket] parameter in log out route.
example:
In case of the Sinatra:
get '/logout' do
session_end! # provided by sinatra-session gem
session[:cas_ticket] = nil # session variable set by CAS server
end
here we are explicitly setting the session[:cas_ticket] to nil, however you can use session.clear in logout route to destroy the current session data .
I am having problems with the session being lost after a redirect in my application. We are using the gem "Koala" to integrate with Facebook. This gem was working fine with Rails 3.0, but not we have upgraded to Rails 3.2.12 and it's failing because the session vanishes after the redirect to Facebook.
I have been investigating and reading a lot about similar problems, but could not get any solution yet. The key here are 2 actions:
1) Publish_event action, where we set the Koala object and connect with Facebook:
def publish_event
...
...
callback = url_for(......................)
#oauth = Koala::Facebook::OAuth.new(Facebook::APP_ID, Facebook::SECRET, callback)
session[:oauth] = #oauth
redirect_to #oauth.url_for_oauth_code(:permissions => ["offline_access", "publish_stream", "manage_pages"])
end
That part works fine and connects to Facebook successfully. Facebook redirects back to our application, and the second action is called:
2) Facebook callback action, where we post the message that we want:
def facebook_callback
token = session[:oauth].get_access_token(params[:code])
#graph = Koala::Facebook::API.new(token)
.....
end
The problem here is that after the redirection to Facebook, the session is not loaded (Rack::Session::Abstract::SessionHash:0x30027e4 not yet loaded), so we can not continue, as we need the original oauth Koala object that created the connection with Facebook and that should be in the session.
I have the sessions configured as an active_record_store, and it works well in the rest of the application, so I am very confused here.
Anyone has come across anything similar ? OR do you have any suggestion how to do this differently ?
Thanks !