Multiple values for OmniAuth.config.full_host - ruby-on-rails

I am trying to configure Omniauth and Devise using Stripe and LinkedIn strategies on my SaaS platform.
As part of the platform each client can have their own unique domain.
I can get LinkedIn working on my client sites, but Stripe requires you list all the callback URL possibilities in their admin interface. To work around this I have set www.myapp.com as the callback url via OmniAuth.config.full_host however this means I have a static callback url and then pass throught he client ID via the state parameter.
This works for Stripe, but then LinkedIn begins to fail as the callback domain no longer matches the originating request.
Is there a way to set OmniAuth.config.full_host for only certain strategies (i.e. only Stripe)?

config/initializers/omniauth.rb
OmniAuth.config.full_host = lambda do |env|
if env['omniauth.strategy'].is_a?(OmniAuth::Strategies::Stripe)
#return url for stripe
else
#return url for others
end
end

Another option might be to override OmniAuth::Stragies::Stripe#callback_url.
#config/initializers/stripe.rb
module OmniAuth
module Strategies
class Stripe
def full_host
#return url for stripe
end
end
end
end
The original OmniAuth::Strategy#full_host method will be used for the other strategies.

Related

Authenticating docusign via Rails API (Omniauth + Devise) + JS Frontend

I'm trying to create an authentication flow using Auth Code Grant where I've added necessary omniauth strategy for Docusign to create /auth/docusign routes in Rails API only application.
Here are the steps followed
I'm issuing a request to the route from VueJS client.
window.open("http://localhost:4000/auth/docusign", "targetWindow", "width=350,height=250")
After user enters credentials and on successful login I'm calling the callback:
class SessionsController < Devise::SessionsController
def docusign
internal_destroy
#success = false
userinfo = request.env['omniauth.auth']
request_info = request.env['omniauth.params']
if userinfo
info = userinfo.info
cred = userinfo.credentials
user = User.find_by(email: info['email']) || User.find_by(id: session[:user_id])
if user
organization = user.organization
organization.organization_providers.where(provider_name: 'Docusign').destroy_all
OrganizationProvider.create(email: info['email'], token_expires_at: Time.at(cred['expires_at']), token_expires_at: Time.now, provider_name: 'Docusign', organization_id: organization.id, token: cred.token)
#success = true
end
end
render 'sessions/docusign'
end
end
I'd like to pass some params (which I'm accessing in the callback as request.env['omniauth.params']) for executing some backend tasks in the method.
When I try window.open("http://localhost:4000/auth/docusign?email='"+email+"'", "targetWindow", "width=350,height=250")
It says that the url doesn't match with any redirect urls
I have also tried passing in redirect_to('/auth/docusign', query: query) but on doing so, it doesn't open in a browser due to CORS.
I'm also trying to set it in session cookie, but since it's an API only server, I'm still working towards setting up cookie store.
Question
Which is the best way to achieve this? To pass some params in the callback and retrieve it.
Then the execution flow continues on the Rails server and the window serves a page with an appropriate response as per authentication status. However during this time, the client window which started the request is not aware of the authentication outcome.
Question
How can I communicate to the VueJS client that the authentication process is completed?
Question
Am I doing the above flow correctly or are there any better ways to achieve the same?
Thanks in advance
You need to log into your DocuSign Developer Account, Click on Admin and go on the left nav down to "API and Keys" where you can find the integration key you set. Did you set one?
If you did, you should find it and then add the redirectUri to the OAuth settings for that key (client ID in OAuth).
That is why DocuSign login tells you that the redirectURI doesn't match. You can add http://localhost:4000/auth to the list and that should work for your local env.
You cannot past custom variables on the redirectUri, it has to match exactly to the one you entered. If you need to pass values to it, there's a way to do that using state.
Here is how the URL should look, notice the &state= part of it:
https://account-d.docusign.com/oauth/auth?
response_type=code
&scope=YOUR_REQUESTED_SCOPES
&client_id=YOUR_INTEGRATION_KEY
&state=YOUR_CUSTOM_STATE
&redirect_uri=YOUR_REDIRECT_URI
&login_hint=YOUR_LOGIN_HINT
You can put whatever you want in there (URI encoded of course) and that value would come back to you when redirected back also with &state= parameter.
This solves the problem and allows you to pass arguments back to your redirect URI.

request.env["omniauth.params"] empty during callback phase when using omniauth-stripe-connect

I am attempting to use the omniauth-stripe-connect strategy to connect user accounts so i can transfer funds directly to their accounts during a purchase.
I have the following in an initializer
Rails.application.config.middleware.use OmniAuth::Builder do
provider :stripe_connect, APP_CONFIG[:stripe_connect_client_id],
APP_CONFIG[:stripe_secret_key],
scope: 'read_write',
stripe_landing: 'register'
on_failure { |env| AuthController.action(:failure).call(env) }
end
and the following in an auth controller
def stripe_connect
result = request.env["omniauth.auth"]
pass_through_params = request.env["omniauth.params"]
...
# do some stuff
end
and I initiate the authentication process with
http://test.lvh.me:3000/auth/stripe_connect?user_id=980190962&user_subdomain=test
During the callback request.env["omniauth.auth"] has correct values, but request.env["omniauth.params"] is always {}. request.env["omniauth.origin"] is also nil.
I have gotten this to work in other situations (facebook oauth integration). At a loss as to why those values are not returned as expected.
The issue has entirely to do with how oauth sets the param values. This is managed through values stored on session. I had thought the values were being forwarded to the authenticating service (in this case Stripe) and returned by that service.
Instead, they get placed in session prior to the call, and appended to the oauth values during the callback phase.
My issue was related to my use of subdomains. I was initiating the request on one subdomain and returning (callback url) to a different subdomain. Since sessions aren't maintain across subdomains (at least with our configuration), the params information was not available.
Reworking the process to use the same subdomain during the request phase and the callbacks phase solved the problem.

How to hit Office 365 API from Ruby?

I am attempting to access the Office 365 API from a Ruby on Rails backend and am having problems.
Whether I use the ruby_outlook gem (github) or follow Microsoft's official Ruby on Rails sample, I am getting 401 unauthorized.
My access_token is being saved using Omniauth and is valid, I checked by pasting it in here.
Am I using the correct access_token? It is over 1400 characters long (1442 to be exact). Can anyone show me an example of how to properly call the Office 365 Mail API from Ruby?
Code Example (using Faraday):
key = #auth[:key]
conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
# Outputs to the console
faraday.response :logger
# Uses the default Net::HTTP adapter
faraday.adapter Faraday.default_adapter
end
response = conn.get do |request|
request.url '/api/v2.0/me/contacts'
request.headers['Authorization'] = "Bearer #{key}"
request.headers['Accept'] = 'application/json'
end
Code Example (using ruby_outlook gem):
client = RubyOutlook::Client.new
key = #auth[:key]
page = 1
view_size = 30
fields = [
'DisplayName',
'EmailAddresses'
]
sort = {:sort_field => 'DisplayName', :sort_order => 'ASC'}
contacts = client.get_contacts key, view_size, page, fields, sort
The exact error that the ruby_outlook gem returns is:
{"ruby_outlook_error"=>401}
The problem is a mismatch between the scopes in your token and the API endpoint you're using. The scope has to match the endpoint.
In your case, you requested a Graph API scope, but you're calling the Outlook API endpoint.
You should only have to register in one place for your client ID and secret: https://apps.dev.microsoft.com. It sounds like you may have also registered an app in the Azure Management Portal (which requires you to specify scopes in the registration itself).
Make sure you're using a client ID from apps.dev.microsoft.com and make sure your scopes are requested as 'https://outlook.office.com' scopes, and you should be good to go.
That Omniauth strategy might require that you register in the Azure Management Portal if they are dependent on Azure's v1 auth endpoints. In that case, forget what I said about apps.dev.microsoft.com and instead change your app registration to use the appropriate permissions from Microsoft Exchange Online.
UPDATE: Based on your comments, that Omniauth strategy DOES require the v1 Azure auth/token endpoints, so you have 2 options if you want to keep using that strategy:
Change your code to use the Graph endpoints. You'll need to use the Faraday option above (ruby_outlook is designed for the Outlook endpoints), and change your URL to https://graph.microsoft.com, and the request.url to /v1.0/me/contacts.
Create a new app registration at https://dev.outlook.com/appregistration, which will create the proper scopes for your code. You'll need an Office 365 account to login to the app registration tool.

Rails authentication across apps/servers

I've been developing my rails apps whilst keeping them as modular as possible. I'm trying to implement different parts underneath as services.
Say an example of Facebook:
a) A MainApp that allows the user to have a wall, posts, etc.
b) A PhotoApp that stores photos, allows the user to see his photos, etc. This is a standalone app that will have a REST API that can be used by MainApp as well.
I was thinking of using OAuth as a Single Sign On solution (as in this tutorial http://blog.joshsoftware.com/2010/12/16/multiple-applications-with-devise-omniauth-and-single-sign-on/) where each app will be authorized via OAuth and will get access to the current user session based on the cookie.
First question: Is this a viable solution?
Second question: I want to be able to call the PhotoApp API from the MainApp server (not from the user's browser). How would authentication work in this situation?
Third question: How would this work if say I had a service that used node.js?
Yes, SSO using OAuth is a viable solution, but it's not the simplest one. When building anything new, OAuth 2.0 is the way to go. The OAuth standards cover a lot of ground.
The primary advantage of OAuth is that it allows users to give 3rd party apps access to their account without disclosing their password to the 3rd party. If you are not seriously providing such interoperability, then OAuth is probably overkill.
Given the complexity, I offer a different pair of solutions:
For Single Sign On
The trick is to share the session ID cookie between hosts within your domain & to use a shared session store (like ActiveRecordStore or a cache-based store.)
Every Rails app has a "secret" that is used to sign cookies. In newer Rails apps this is located in /config/initializers/secret_token.rb. Set the same secret token in each application.
Then, configure the session to allow access from all subdomains:
AppName::Application.config.session_store :active_record_store, :key => '_app_name_session', :domain => :all
For Internal API calls
Use a good shared secret to authenticate over HTTPS connections. Pass the secret in the "Authorization" header value.
You can use the shared secret easily with other architectures (like node.js). Just make sure you always use HTTPS, otherwise the shared secret could be sniffed on the network.
You could look at a Service Oriented Architecture solution as proposed by Jeremy Green at Octolabs during the 2014 RailsConf.
The blog post with all the resources (repos, demos, etc.) is located here: http://www.octolabs.com/so-auth
And the video that explains everything is here: http://www.youtube.com/watch?v=L1B_HpCW8bs
This centralized SSO is no simple task but Jeremy has done an excellent job talking about Service Oriented Architecture and sharing exactly how you might put this system together.
I recently had a similar problem of wanting to share session data between Rails and an Erlang app. My solution was to write a Rack::Session::Abstract::ID class that stored sessions in Redis as hash vaules. It doesn't call Marshal.dump on String types. This allows non-ruby applications to use some of the session values if they have the session_id.
require 'rack/session/abstract/id'
class MaybeMarshalRedisSession < Rack::Session::Abstract::ID
def initialize(app, options = {})
#redis = options.delete(:redis) || Redis.current
#expiry = options[:expire_after] ||= (60 * 60 * 24)
#prefix = options[:key] || 'rack.session'
#session_key = "#{#prefix}:%s"
super
end
def get_session(env, sid)
sid ||= generate_sid
session = #redis.hgetall(#session_key % sid)
session.each_pair do |key, value|
session[key] = begin
Marshal.load(value)
rescue TypeError
value
end
end
[sid, session]
end
def set_session(env, sid, session, options={})
#redis.multi do
session.each_pair do |key, value|
# keep string values bare so other languages can read them
value = value.is_a?(String) ? value : Marshal.dump(value)
#redis.hset(#session_key % sid, key, value)
end
#redis.expire(#session_key % sid, #expiry)
end
sid
end
def destroy_session(env, sid, option={})
#redis.del(#session_key % sid)
generate_sid unless options[:drop]
end
end
You can use this from rails with:
MyApp::Application.config.session_store MaybeMarshalRedisSession
From Rack with:
use MaybeMarshalRedisSession
And from elsewhere with:
redis.hgetall("rack.session:#{session_id}")
If you want to call PhotoApp from your MainApp or Node.js you can make a HTTP request that includes your user's session cookie.

Using devise "rememberable" without cookies

I have a working Rails site that uses devise to manage users. For session management, I am using devise's rememberable strategy, which stores and retrieves encrypted authentication information from a user's cookie.
I'm implementing a multi-photo upload widget that uses flash. Flash does not support sending cookies along with requests. This is a problem with multiple multi-upload flash+javascript libraries, so fixing this shortcoming is probably not feasible.
So my question is: can I successfully authenticate to devise/rememberable without using cookies? And if so, how?
More details
Devise/rememberable depends on the value of remember_token within the cookie. If I could fool Rails into thinking that the value was supplied as a cookie (e.g. request.cookies['remember_token'] = '...'), my problem would be solved. Devise/rememberable would find the correct value there, unpack it, and successfully authenticate. However, the request.cookies hash is apparently read-only. Writing to the hash is silently ignored. Example (debug console from an incoming POST request):
>> request.cookies['remember_token'] = 'a string'
=> "a string"
>> request.cookies['remember_token']
=> nil
>> request.cookies
=> {}
I'm using (or trying to use) the FancyUpload v3 widget.
How about overriding Devise slightly?
Based on Devise 1.2.rc something like this should work:
module Devise
module Strategies
class Rememberable
def remember_cookie
# your code to get the hashed value from the request
end
end
end
end
Alternatively, you could add a new (subclassed) strategy:
module Devise
module Strategies
class RememberableParameter < Rememberable
def remember_cookie
# your code to get the hashed value from the request
end
end
end
end
Warden::Strategies.add(:rememberable_parameter, Devise::Strategies::Rememberable)
Or, look into Token Authenticatable:
Token Authenticatable: signs in a user based on an authentication token (also known as
"single access token"). The token can be given both through query string or
HTTP Basic Authentication
There's more about it here:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/token_authenticatable.rb
Good luck!

Resources