Rails - Using omniauth-saml with multiple IDPs - ruby-on-rails

What I'm trying to have in the end is the ability to login normally with devise OR choose to login with SAML. So I read that if I integrate omniauth and saml, then omniauth and devise, I could achieve that.
My problem is, that I have different IDPs that I would like to choose from. So I don't have one :idp_sso_target_url, but many. So my question is how can I dynamically change the value of the target_url. Currently the omniauth-saml gem defines this value in the config/initializers directory..
Thank you,

You can store settings for every provider in db, and then configure omniauth in the setup phase at request-time. For example:
SETUP_PROC = lambda do |env|
request = Rack::Request.new(env)
user = User.find_by_subdomain(request.subdomain)
env['omniauth.strategy'].options[:consumer_key] = user.consumer_key
env['omniauth.strategy'].options[:consumer_secret] = user.consumer_secret
end
use OmniAuth::Builder.new do
provider :twitter, :setup => SETUP_PROC
end
See https://github.com/intridea/omniauth/wiki/Setup-Phase for more information.

Using multiple SAML IDPs with Devise + OmniAuth:
Follow this official guide for Single IDP.
https://github.com/omniauth/omniauth-saml#devise-integration
Once you have your SP working with single IDP, do following tweaks
In devise initializer
config.omniauth :first, {
name: :first,
strategy_class: ::OmniAuth::Strategies::SAML,
#Rest of the config as per omniauth-saml guide
assertion_consumer_service_url: '/users/auth/first/callback'}
config.omniauth :second, {
name: :second,
strategy_class: ::OmniAuth::Strategies::SAML,
#Rest of the config as per omniauth-saml guide
assertion_consumer_service_url: '/users/auth/second/callback'}
In Users::OmniauthCallbacksController, add actions named first and second in instead of saml as suggested in official guide.
In your User model:
devise :omniauthable, omniauth_providers: [:first, :second]
If all configured correctly, you now have your SP configured for two IDPs.

Rails.application.config.middleware.use OmniAuth::Builder do
provider :saml,
name: "first",
assertion_consumer_service_url: "/auth/first/callback",
issuer: "your-app",
idp_sso_target_url: "first.com/idp"
idp_cert_fingerprint: "E7:91:B2:E1:...",
name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
provider :saml,
name: "second",
assertion_consumer_service_url: "/auth/second/callback",
issuer: "your-app",
idp_sso_target_url: "second.com/idp",
idp_cert_fingerprint: "E7:91:B2:E1:...",
name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
end

If using the application in a federation context there will be most likely a metadata source, such as prescribed in the saml2int.org profile. This metadata has the data to populate IDP discovery (and automatically configure all the IDPs). It seems that omniauth-saml does not support the SAML Metadata specification, therefore some kind of SAML proxy is the alternative.

Related

Ruby OAuth2.0: client credential type has unsupported client authentication method

I am using OAuth2 gem, for making a client_credential authentication. My code is as below,
require 'oauth2'
client = OAuth2::Client.new("my_client_id", "my_client_secret", :site => "my_site_url", :token_url => "oauth2/token")
client.client_credentials.get_token
When I execute above code block, it respond with below error,
OAuth2::Error (invalid_client: Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method))
{
"error":"invalid_client","error_description":"Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)",
"error_hint":"The OAuth 2.0 Client supports client authentication method "client_secret_basic", but method "client_secret_post" was requested.
You must configure the OAuth 2.0 client's "token_endpoint_auth_method" value to accept "client_secret_post".","status_code":401}
I checked the using 'net/http' library, and my client_id & client_secrets are valid and working.
The only problem I see is with the authentication method as said in hint of above message,
The OAuth 2.0 Client supports client authentication method "client_secret_basic", but method "client_secret_post" was requested. You must configure the OAuth 2.0 client's "token_endpoint_auth_method" value to accept "client_secret_post"
What I want to know is?
How OAuth2 gem decide on using client_secret_post vs client_secret_basic? I mean How can I request with client_secret_basic in OAuth2 gem?
If not above then, How should I specify token_endpoint_auth_method to accpet client_secret_post?
OK, so finally I cleared these points.
OAuth2 gem does make a request to OAuth server with --token_endpoint_auth_method set to 'client_secret_post'.
While registering an client with OAuth server we will have to set token_endpoint_auth_method to 'client_secret_post', so that it will work.
In my case I was using Hydra, so I used below command to create a client:
hydra clients create --endpoint <OAuth server url> --id CLIENT_ID --secret CLIENT_SECRET \
--token-endpoint-auth-method 'client_secret_post' -g client_credentials
Now, using these CLIENT_ID and CLIENT_SECRET with oauth2 works.
But still one point which is unclear - can I make a request with token_endpoint_auth_method set to client_secret_basic using oauth2 gem.
I also encountered the same issue.
Please add or change this client option setting in your client code.
:auth_scheme => :basic_auth
The default settings is below.
:auth_scheme => :request_body
I have excerpted a part of the OAuth2::Client code.
Please check it.
require 'faraday'
require 'logger'
module OAuth2
# The OAuth2::Client class
class Client # rubocop:disable Metrics/ClassLength
attr_reader :id, :secret, :site
attr_accessor :options
attr_writer :connection
# #option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
def initialize(client_id, client_secret, options = {}, &block)
opts = options.dup
#id = client_id
#secret = client_secret
#site = opts.delete(:site)
ssl = opts.delete(:ssl)
#options = {:authorize_url => '/oauth/authorize',
:token_url => '/oauth/token',
:token_method => :post,
:auth_scheme => :request_body, # <-- Here !!!
:connection_opts => {},
:connection_build => block,
:max_redirects => 5,
:raise_errors => true}.merge(opts)
#options[:connection_opts][:ssl] = ssl if ssl
end
Sample snippet is here https://gist.github.com/mtoshi/cd74f57631805fb1b2290137f58dac9f
If you use a middleware it probably use the client_secret_basic to make a request you only need to change the configuration of that something similar to this. I use nextauth middleware.
client: {
token_endpoint_auth_method: 'client_secret_post'
}

How to get omniauth info from Devise

When we configure Devise we put in config/initializers/devise.rb something like this:
config.omniauth :google_oauth2, "[client_id].apps.googleusercontent.com", "[client_secret]"
I'm curious how to use this info (client_id and client_secret) inside the app?
For example,
flow = Google::APIClient::InstalledAppFlow.new(
:client_id => client_secrets.client_id,
:client_secret => client_secrets.client_secret,
:scope => [YOUTUBE_READONLY_SCOPE]
)
I'd like to get this info from Devise instead of hardcoding it.
You could add the client_id and client_secret in an yml file called google.yml (for example) and in devise.rb you could have something like:
config_google = YAML.load_file("#{Rails.root}/config/google.yml")
config.omniauth :google_oauth2, config_google["client_id"], config_google["client_secret"]
Same thing goes if you want to use the config outside the initializer. Just load the yml and use its contents.
Inside the yml you can have different keys for each environment (development, production, etc). Just make sure you load it properly.
YAML.load_file("#{Rails.root}/config/google.yml")[Rails.env] # for example

Oauth2 Login for Facebook, Linkedin and Google Stopped Working with Devise and Omniauth, But Still Works for LinkedIn and Twitter

I have a site that is configured to work with multiple Oauth2 API's using Devise with Omniauth and has been functioning normally until last week. Currently login with Twitter and Github still function normally; however, Facebook, LinkedIn and Google are giving me an error stating that the Redirect URI doesn't match. The Error Messages read:
Facebook:
ERROR -- omniauth: (facebook) Authentication failure! invalid_credentials: >OAuth2::Error, :
{"error":{"message":"Error validating verification code. Please make sure your >redirect_uri is identical to the one you used in the OAuth dialog request","type":"OAuthException","code":100,"fbtrace_id":"XXXXXXXXXX"}}
LinkedIn:
ERROR -- omniauth: (linkedin) Authentication failure! invalid_credentials: >OAuth2::Error, invalid_request: missing required parameters, includes an invalid parameter value, parameter more than once. : Unable to retrieve access token : appId or redirect uri does not match authorization code or authorization code expired
{"error_description":"missing required parameters, includes an invalid parameter value, parameter more than once. : Unable to retrieve access token : appId or redirect uri does not match authorization code or authorization code expired","error":"invalid_request"}
Google
ERROR -- omniauth: (google_oauth2) Authentication failure! invalid_credentials: >OAuth2::Error, redirect_uri_mismatch:
{
"error" : "redirect_uri_mismatch"
}
I went reviewed the requests that were sent for all three of these in the Chrome Developers Console and the redirect uri for the callback matches the uri that is registered with each API (Which has not changed since it was working).
The challenge with back tracking this error is I am not 100% sure when these stopped working as I was logging in directly or using the Github login during recent integration tests as I installed new functionality. (Big Lesson Learned!) One of the significant changes that could be impacting this is that I integrated the Traceable extension for Devise which had me require the Warden Gem. However, I removed both the Traceable and Warden configuration and restored the user model and config files to their previous state and I am having the same issue.
I would generally prefer to provide more code samples but to be honest, I am not sure what code to start with. I am hoping that someone has experienced a similar problem and can point in the right direction to start.
To Start, below is my Devise Initializer with Comments Removed to Shorten
Devise.setup do |config|
config.mailer_sender = 'no-reply#' + ENV['DOMAIN_NAME']
config.mailer = 'Devise::Mailer'
require 'devise/orm/active_record'
config.case_insensitive_keys = [:email]
config.strip_whitespace_keys = [:email]
config.skip_session_storage = [:http_auth]
config.stretches = Rails.env.test? ? 1 : 10
config.allow_unconfirmed_access_for = 10.days
config.reconfirmable = true
config.confirmation_keys = [:email]
config.remember_for = 2.weeks
config.expire_all_remember_me_on_sign_out = true
config.password_length = 8..72
config.email_regexp = /\A[^#]+#[^#]+\z/
config.reset_password_keys = [:email]
config.reset_password_within = 6.hours
config.sign_in_after_reset_password = true
config.sign_out_via = :get
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
require "omniauth-google-oauth2" # Added Based on Response to Another Stackoverflow Issues - Did Not Help.
OMNIAUTH = YAML.load(File.read(File.expand_path('../../omniauth.yml', __FILE__))).deep_symbolize_keys
OMNIAUTH.each_value do |provider|
config.omniauth provider[:reference].to_sym, ENV[provider[:key_ref]], ENV[provider[:secret_ref]], { :scope => provider[:scope] }
end
end
The omniauth.yml file that is loaded looks like this:
facebook: { reference: "facebook",
name: "Facebook",
scope: "email, public_profile, user_birthday",
key_ref: "FACEBOOK_KEY",
secret_ref: "FACEBOOK_SECRET" }
twitter: { reference: "twitter",
name: "Twitter",
scope: "r_fullprofile, r_emailaddress",
key_ref: "TWITTER_KEY",
secret_ref: "TWITTER_SECRET" }
linkedin: { reference: "linkedin",
name: "LinkedIn",
scope: "r_basicprofile r_emailaddress",
key_ref: "LINKEDIN_KEY",
secret_ref: "LINKEDIN_SECRET" }
github: { reference: "github",
name: "GitHub",
scope: "user, public_repo",
key_ref: "GITHUB_KEY",
secret_ref: "GITHUB_SECRET" }
google: { reference: "google_oauth2",
name: "Google",
scope: "email, profile",
key_ref: "GOOGLE_KEY",
secret_ref: "GOOGLE_SECRET" }
I had exactly similar issue, facebook working, linkedin and google - not.
After some digging/googling i was able to fix my issue by downgrading:
gem 'omniauth-oauth2', '1.3.1'
So my Gemfile looks like:
gem 'devise'
gem 'koala'
gem 'omniauth-oauth2', '1.3.1'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem 'omniauth-linkedin-oauth2'
I went through and updated all of the Omniauth Gems as there was recent version revisions and all of the issues have been resolved.

When connecting to Linkedin with OAuth, is it possible to get the user's non-primary email addresses?

I'm using Devise and the omniauth-linkedin gem to allow users to log into my Rails app with a LinkedIn account (Rails 4.1.8, Devise 3.4.0, omniauth-linkedin 0.2.0). I've had no trouble getting the user's primary email from LinkedIn, but I'm wondering, is it possible to get a list of ALL the emails associated with the user's LinkedIn account, including non-primary emails?
The LinkedIn docs (https://developer.linkedin.com/documents/authentication) don't say how to do this, but they also don't say that it's impossible.
If it IS possible, what scopes/fields do I need to add to my Devise config? Currently the relevant line in config/devise.rb looks like this:
config.omniauth :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'],
scope: 'r_fullprofile r_emailaddress r_network w_messages',
fields: ["id", "email-address", "first-name", "last-name", "headline",
"industry", "picture-url", "public-profile-url", "location",
"connections"]
The LinkedIn Profile API does not expose email addresses other than the primary one associated with a member profile.

Omniauth-google-oauth2 not correctly passing "scope" for oauth

First, the context: I'm writing a little web app to do some custom Google Analytics reporting for me.
In my Gemfile:
gem 'omniauth'
gem 'omniauth-google-oauth2'
In my config/initializers/omniauth.rb:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV["GOOGLE_KEY"], ENV["GOOGLE_SECRET"]
{
:scope => "userinfo.email,userinfo.profile,analytics.readonly",
:approval_prompt => "auto"
}
end
When I get to the auth screen on google, it's only asking me for userinfo.email and userinfo.profile permissions, and nothing for analytics.readonly.
Looking at the URL when I'm auth'ing, I can see that it's only requesting the first two permissions. If I manually add the analytics permission, it grants the correct permissions. So, I've narrowed the issue down to
How I'm passing scope to the omniauth-google-oauth2 strategy, or
How the strategy is handling/ignoring the scope hash.
Also, I have double-checked that the Analytics API is turned on for my OAuth keys in the Google API Console.
Poorly formatted code, I was missing a comma after ENV["GOOGLE_SECRET"]. Below is the fixed code.
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV["GOOGLE_KEY"], ENV["GOOGLE_SECRET"],
{
:scope => "userinfo.email,userinfo.profile,analytics.readonly",
:approval_prompt => "auto"
}
end

Resources