Devise/OmniAuth Override default callback url - ruby-on-rails

I'm using Devise 3.5 with Omniauth in a Rails 4 app. I've created an integration with Facebook that allows a user to connect their Facebook account to my app. Currently when the user clicks the connect button, they're sent to /user/auth/facebook and then redirected to the callback url that Omniauth generates: /user/auth/facebook/callback. What I'd like to do is manually override this callback url in some cases - meaning that I don't want to override it in an initializer - with a fully qualified url. For example, if a user starts out on http://www.example.com/ I might want to override the default callback url with http://app.example.com/user/auth/facebook/callback.
My app has dynamic subdomains and a user will (almost) always begin the authentication process on a subdomain. Unfortunately it seems that Facebook doesn't support wildcards in oauth redirect urls, which is why I want the ability to detect if a user is on a subdomain and adjust the callback url to something that I have whitelisted on my Facebook app so that the authorization process succeeds.
From what I've read, the url helper omniauth_authorize_path accepts additional arguments to be passed on as parameters. I've tried passing a custom callback path in like so, but without success:
user_omniauth_authorize_path(:facebook, callback_path: #custom_callback)
I've also tried changing callback_path to redirect_url and redirect_uri, but nothing seems to work. When I look at the link that's generated, it does indeed include the callback as a parameter in the url, but when I click the link, I'm redirected back to the default callback url instead of the custom callback url.

Here's how I solved this problem. I'm sure there are other ways, but this seems like the simplest most elegant solution I could come up with.
In config/routes.rb I set up an auth subdomain. All my Oauth connect requests will start on different subdomains and then Facebook is set up to forward those users back to the auth.example.com subdomain.
constraints AuthRedirect do
devise_scope :contact do
get '/auth/facebook/callback' => 'omniauth_callbacks#facebook'
post '/auth/facebook/callback' => 'omniauth_callbacks#facebook'
end
end
Here is /lib/auth_redirect.rb. This just checks if the subdomain is auth and captures that traffic. This is placed at the top of my routes list so as to take precedence over other subdomains.
class AuthRedirect
def self.matches?(request)
request.subdomain.present? && request.subdomain == 'auth'
end
end
Then in my client, when a user clicks the Connect with Facebook button, I send them to /auth/facebook?contact_id=<id>. From here Devise directs them to Facebook, which then redirects them back to https://auth.example.com/.
Then in OmniauthCallbacksController#facebook I can pull the user's id from the omniauth params like so:
auth = env["omniauth.auth"]
contact = Contact.find(env['omniauth.params']['contact_id'])
From here I can persist the credentials to the database and the redirect the user back to the appropriate subdomain. This solution avoids problems with CSRF tokens and more importantly does not require me to use Ruby/ERB to build the omniauth authorize path that the user is sent to when they click the connect button.

have you tried with redirect_uri ?
user_omniauth_authorize_path(:facebook, redirect_uri: #custom_callback)
EDIT: sorry I missed the second part of your post.
I actually have the same problem in production but it works perfectly on a staging environment. The only difference is about the callback url on staging which has one more subdomain *.staging.domain.com
By the way you can provide a static callback_url in the devise initializer file:
config.oaumniauth :facebook, ..., callback_url: 'url right here'
I'm on this issue this yesterday.
Either I provide a static callback url but facebook raises me an CRSF error:
omniauth: (facebook) Authentication failure! csrf_detected: OmniAuth::Strategies::OAuth2::CallbackError, csrf_detected | CSRF detected
Or I let devise set the callback_url dynamically which gonna look like
https://*.domain.com/DEVISE_MODELS/auth/facebook
and in this case I get a straight non matching/whitelisted callback url during FG loggin in process.
EDIT2:
GOOD! I made it. I'm able to get oauth login in with wildcard subdomain.
Provide a static callback_url in your devise initializer
add the domain to your session store as :
domain: ".domain.com"
With that I'm getting neither CRSF error nor nunmatching CB url/whitelisted.
Hope it'll work for you !

Related

Session empty after redirect

I've a React JS app, which makes this request to my back-end API. i.e
window.location = "https://my-server.com" + "/gmail/add_account";
cannot set HTTP headers for window.location see this
this server endpoint redirects to Google OAuth page, which returns a response to my redirect_uri.
def add_account
# no auth headers sent here, because front-end has used window.location
gmail_service = GmailService.new
session[:uid] = params["uid"]
redirect_to gmail_service.generate_authorization_url()
end
def oauth_postback
# session object is {} here
# Since there are no authorization headers, I cannot identify my app's user
# How can I identify my app's user here?
end
The problem I'm facing is that when the OAuth flow sends the response to my redirect_uri it does not return include any authorization header, due to which I'm unable to identify which user of my app has launched this OAuth flow.
I've tried setting up a session variable in the /gmail/add_account endpoint, which works fine. After this endpoint redirects to the OAuth screen, and the Oauth flow sends a response to my Oauth redirect_uri, there my session object is {}.
How can I implement this flow such that I know which user has launched this OAuth flow?
You have basically two options:
the state parameter
The state parameter is part of the OAuth2 spec (and is supported by Google). It's a random string of characters that you add to the authorization URL (as a query parameter), and will be included when the user is redirected back to your site (as a query parameter). It's used for CSRF protection, and can also be used to identify a user. Be sure that if you use it, it's a one-time value (e.g. a random value that you store in your db, not the user's ID).
sessions with cookies
If the user has previously logged in, you should be able to identify them by their session cookie. It sounds like this is the approach you're currently taking, but the session is getting reset.
It's difficult to debug this without knowing more about your stack/code, but a good first step would be just trying to load your callback URL without the redirection to Google to see the session object is still empty. If so, that would indicate an issue with how you've implemented sessions generally and not something specific to this flow.
As a note, based on the code you've shared, I'm not sure how params["uid"] is getting set if you're doing a redirect without any query parameters or path parameters.
Finally, you may consider using a managed OAuth service for something like this, like Xkit, where I work. If you have a logged in user, you can use Xkit to connect to the user's Gmail account with one line of code, and retrieve their (always refreshed) access tokens anywhere else in your stack (backend, frontend, cloud functions) with one API call.

How to use Omniauth Asana with Rails API only app

I have a Rails 5 API only app and an Angular JS Frontend app and would like to integrate with Asana API. I'm using the ruby-asana, omniauth and omniauth-asana gems.
I start the request using Asana's JS library like so:
var client = Asana.Client.create({
clientId: 172706773623703,
clientSecret: '<client_secret>',
redirectUri: '<redirect_url>'
});
client.useOauth({
flowType: Asana.auth.PopFlow
});
And the above does redirect me to Asana where I can login. On the redirectUri I'm giving a backend route (Rails 5 API only) which should handle the remaining on the authentication (using the JS only I get only a temporary token that cannot be self renewed meaning the user will have to authenticate every time the token expires. This is if I understood the documentation correctly).
So, on the controller I've created to handle the route, I have the following (from an example on Asana's documentation):
require 'omniauth-asana'
use OmniAuth::Strategies::Asana, <secret>, <secret>
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh!
$client = Asana::Client.new do |c|
c.authentication :oauth2, access_token
end
Now, the above doesn't work because 1) there's no request.env as this is an API only app, so I've followed the instruction on Omniauth and have added the following to my config/application.rb:
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
Now, in the request.headers I have _interslice_session which has some numbers. How can I create a Asana client with the above?
Any ideas?
OK, I think I see what you're attempting to do here; I think the best way forward is to start with how OAuth's Authorization Code Grant happens in general, then move into specifics for OmniAuth.
You send the user to a URL that Asana owns; that is, your goal is to get the user to visit a particular url. For Asana, this is https://app.asana.com/-/oauth_authorize. (Note that we respond with an error if you don't sent a correct client_id param, but feel free to check that link if you want). Do not send the client_secret during this request - it is intended to never be involved in client-side code, as this is insecure.
If they agree to give access, Asana sends back a redirect request to the user's browser with a short-lived code. That then means that your server will be called from the user's browser with this code as a parameter, so has to handle a new incoming request from the browser to whatever you specified as your redirect URI. Also, this location must be accessible by all users of your integration wherever they are.
You send this code from your server as a POST request to https://app.asana.com/-/oauth_token with your client_secret to Asana for a refresh token. This is where your application actually asks for credentials; the token given in the previous phases simply acknowledges that for a short time, the user has given your app permission to ask for these credentials, and your client_secret assures Asana that, for this server-side request, your app really is yours (it's like your application's password).
We send back an access_token which represents (approximately) a client-user credential pair that is valid for an hour.
You use these credentials to access our API on behalf of this user. We also send back a refresh_token which is long-lived, and used to get new short-lived access_tokens after they expire in a very similar way.
OK, so how this works with OmniAuth if I grok it correctly is that it expects to handle almost all of it. I'll be working through our omniauth example in our ruby-asana client library here: https://github.com/Asana/ruby-asana/blob/master/examples/omniauth_integration.rb
You set up OmniAuth with your client id and client secret
use OmniAuth::Strategies::Asana, <client_id>, <client_secret>
A request comes in, and you don't have credentials for it.
get '/' do
if $client
...
else
'sign in to asana'
end
end
The user clicks the sign in link, which (code omitted) sends them to the sign_in endpoint. This endpoint issues a redirect to /auth/asana
The browser requests /auth/asana from our server. If you look at that example, it's not implemented in our code. That's because the /auth/:provider is magically handled by OmniAuth.
This is where all the magic happens. OmniAuth handles the entire login flow above: send browser to our oauth_authorize above, then receive the callback and sticks the relevant params in the environment such that it knows "we just got the short lived code". By the time these lines get hit:
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
you are inside a callback that OmniAuth has intercepted, gotten the needed creds, and set the creds in the environment. You shouldn't have to handle the oauth callback and token exchange manually.
Now, with the code you provided, I notice a few things right off:
You are causing the popup cycle to happen client side. It may be (and I strongly suspect) that this won't work with OmniAuth - it expects to handle the whole OAuth flow.
Based on the code snippet you provided, you aren't serving this out of a request-response cycle in the controller, rather, it appears that this is in the controller body and not out of an instance method. It may be a typo, but this needs to be in a method that is called back to outside of Rails (that is, a route must point to this a controller method that Asana can use to handle the browser request).
You shouldn't have to look at request.headers, I think - I'm not sure what the issues might be with request.env, but I suspect they may be unrelated to the API-only nature of your app. Are you sure that this is because it's API-only? After adding in the middleware, did you double-check that you can't access request.env? My hunch would be that persistent data in request.env will still be there, only it would require on the middleware being added in to do this. The instructions on OmniAuth simply say that you need to have a session store for your API - which makes sense to me, because APIs don't necessarily need to store state across requests, OmniAuth is telling you to put a session store back in.
I know this is a lot of info, but hopefully it helps you get on the right track. Cheers!

HTTP basic authentication over Devise

I am trying to use HTTP Basic Authentication over Devise for my Rails app. I have done following settings ->
config.http_authenticatable = true in the devise initializer
And
:database_authenticatable strategy in my USER model.
When I try to access a web service
mysite.com/user/list.json?email=test#mysite.com&password=test123
The username password is not recognized. A credentials box pops up where on entering the credentials I am authenticated to use the data.
How can I provide the credentials in the URL and avoid the pop up prompt?
HTTP Basic Auth, is passed through HTTP Headers, not GET or POST params.
I found a way of doing this-->
http://username:password#test.com/users/list.json?

omniauth-instagram won't include my client-id as part of the authorization url

So I'm trying to make an app where I want to allow users to login using their Instagram accounts. This is a Rails app. I'm mostly following Railscast 241 for doing this except that I use Instagram API instead of Twitter API. I'm not using devise.
I installed the gem 'omniauth-instagram' and I have the following in one of my initializers -
Rails.application.config.middleware.use OmniAuth::Builder do
provider :developer unless Rails.env.production?
provider :instagram, ENV['MY_CLIENT_ID'], ENV['MY_CLIENT_SECRET']
end
The problem is that when I direct the user to the 'auth/instagram' path the request does not contain my client-id (I check the Chrome debugging tools > Network to make sure of this). And as a result, although it takes the user to the login page, but then it fails and gives the following response -
{"code": 400,
"error_type": "OAuthException",
"error_message": "You must include a valid client_id, response_type, and redirect_uri parameters"}
So instead of making the request o 'auth/instagram' path I direct the user to the actual autorization URL i.e.
https://api.instagram.com/oauth/authorize/?client_id=CLIENT-ID&redirect_uri=REDIRECT-URI&response_type=code
And all goes according to the plan. Except that I don't get the user information as a part of request.env['omniauth.auth'] inside my controller method (after being successfully redirected to the right URL). Infact request.env hash does not have omniauth.auth as one of its keys. The fix to this is that I'll manually have to write a curl -F query to the API to get the user information.
But that sounds like too much work and I feel there must be something that I might have been doing wrong. Why isn't the gem making the correct request with my provided client_id? and why isn't 'omniauth.auth' get properly populated as part of the params?
Some relevant resources -
Instagram API authentication page
omniauth-instagram gem

Rails 3 + Omniauth: pass extra params to the api server (DoorKeeper)

I am creating an API for my application. On one side, there is the API server (and the main app) and on the other, the client. The sever uses DoorKeeper to secure the API using OAuth 2.0 (basically turning the main app into an OAuth 2.0 provider) and the client is using OmniAuth with a custom strategy for my app.
The main app uses multitenancy using subdomains; so every client has its own subdomain. There is also a oauth subdomain that is routed to the DoorKeeper interface.
When a user clicks on the "Log in with my app" link, he gets redireced to the oauth subdomain. If he is not logged in to the main app, he needs to get redirected to the login page under the correct subdomain. So I need to pass the client's account name to the server so that DoorKeeper knows to which subdomain to redirect to.
How can I achieve that please?
I've researched on the subject and found out how to pass to OmniAuth params that will get passed to the callback action. Will those params be available to the server?
EDIT: I am not using Devise!
EDIT 2: Here is some code.
Client app session controller create action (log in with my app)
def set_client
self.current_client = Client.find(params[:client][:name])
redirect_to "/auth/catapult?client=#{self.current_client.account_name}"
end
As you can see, I append the client param to the OmniAuth route, but this param is not passed to the server app (DoorKeeper), so I have no idea where to redirect to on the server app.
DoorKeeper config
resource_owner_authenticator do
p params
User.find_by_id(session[:user_id]) || redirect_to(log_in_path)
end
In the redirect above, I need to specify the client's account name as subdomain, but I don't have this info (client's account name) since the params hash does't contain the client's account name that I passed (the client param)
I found out how to fix my problem. I had to dig in deeper into the OmniAuth source code. what I had to do is override the request_phase method in my custom strategy as follow:
def request_phase
redirect client.auth_code.authorize_url({:redirect_uri => callback_url, :catapult_client => request.params["client"]}.merge(authorize_params))
end
Where :catapult_client is, add any extra params you want to pass and it just works!
The simplest way is to pass the place-to-redirect-to-after-authenticating-successfully as a query param when they are redirected to the login page, so it's there as part of the GET request's querystring. Store it, and on a successful auth, redirect them there. You don't need to involve this data in the OAuth process at all.
Of course, I'm assuming that they all start at their subdomain too.
Edit:
When a user clicks on the "Log in with my app" link, he gets redireced to the oauth subdomain.
Assuming the user starts at mysubdomain.yourapp.com, they click on the "Log in with my app" link.
The link also contains a query parameter with the subdomain in it, so oauth.yourapp.com?redirect=mysubdomain.yourapp.com (or just oauth.yourapp.com?redirect=mysubdomain)
The user arrives at oauth.yourapp.com. The app stores the query parameter. The user puts in their details or is redirected to a serviceā€¦
The OAuth process is finished, the user has been authenticated.
Redirect the user back to the redirect parameter stored earlier.
This is how I do it, just not with Rails, but I don't see why you couldn't use this process with any framework. This, as I mentioned, depends on the user starting on the correct subdomain.

Resources