Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi tenant support with omniauth-multi-provider #5

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ gem 'jbuilder', '~> 2.7'
# gem 'bcrypt', '~> 3.1.7'

gem 'devise'
gem 'omniauth-multi-provider'
gem 'omniauth-saml', '= 1.10.3'
gem "omniauth-rails_csrf_protection"

# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
Expand Down
25 changes: 21 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ GEM
regexp_parser (~> 1.5)
xpath (~> 3.2)
childprocess (3.0.0)
concurrent-ruby (1.1.7)
concurrent-ruby (1.1.8)
crass (1.0.6)
devise (4.7.3)
bcrypt (~> 3.0)
Expand All @@ -89,9 +89,10 @@ GEM
ffi (1.14.2)
globalid (0.4.2)
activesupport (>= 4.2.0)
hashie (4.1.0)
i18n (1.8.7)
concurrent-ruby (~> 1.0)
jbuilder (2.10.1)
jbuilder (2.11.0)
activesupport (>= 5.0.0)
listen (3.4.1)
rb-fsevent (~> 0.10, >= 0.10.3)
Expand All @@ -113,6 +114,17 @@ GEM
nokogiri (1.11.1)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-multi-provider (0.2.1)
omniauth
omniauth-rails_csrf_protection (0.1.2)
actionpack (>= 4.2)
omniauth (>= 1.3.1)
omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.9)
orm_adapter (0.5.0)
pg (1.2.3)
public_suffix (4.0.6)
Expand Down Expand Up @@ -160,6 +172,8 @@ GEM
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
rubyzip (2.3.0)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
Expand All @@ -183,7 +197,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
thor (1.0.1)
thor (1.1.0)
tilt (2.0.10)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
Expand All @@ -197,7 +211,7 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webdrivers (4.4.2)
webdrivers (4.5.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0)
Expand All @@ -223,6 +237,9 @@ DEPENDENCIES
devise
jbuilder (~> 2.7)
listen (~> 3.3)
omniauth-multi-provider
omniauth-rails_csrf_protection
omniauth-saml (= 1.10.3)
pg (~> 1.1)
puma (~> 5.0)
rack-mini-profiler (~> 2.0)
Expand Down
15 changes: 14 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
class ApplicationController < ActionController::Base
before_action :authenticate_user!, except: :index
before_action :authenticate_user!, only: :logged_in
protect_from_forgery with: :exception

TENANTS = {
'example.com' => "08909493-cc6f-4a67-9986-f8f4452ba1d4",
'your-email-domain.com' => "4eaff58f-40a2-4ebe-b746-a9dbe2103864"
}

def idp_login
domain = params[:email].split('@').last
idp_id = TENANTS[domain]

render json: { identity_provider_id: idp_id }
end

def index
end
Expand Down
12 changes: 12 additions & 0 deletions app/controllers/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery with: :exception, except: :saml

def saml
auth_hash = request.env['omniauth.auth']
@user = User.create_or_find_by!(email: auth_hash['uid'])

sign_in(@user)

redirect_to(:logged_in)
end
end
38 changes: 38 additions & 0 deletions app/javascript/packs/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
document.addEventListener("turbolinks:load", function() {
const loginForm = document.querySelector("#login-form");
loginForm.addEventListener("ajax:success", (event) => {
const [data, ..._rest] = event.detail;
if (data.identity_provider_id) {
return samlLogin(data.identity_provider_id)
}

passwordLogin(loginForm)
});
});


function samlLogin(idpId) {
const form = document.createElement("form");
const tokenInput = document.createElement("input");
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
tokenInput.value = csrfToken;
tokenInput.name = "authenticity_token";

form.action = `/users/auth/saml/${idpId}`;
form.hidden = true;
form.method = "POST";

form.appendChild(tokenInput);
document.body.appendChild(form);

form.submit();
}

function passwordLogin(loginForm) {
const passwordInput = document.createElement("input");
passwordInput.name = "password";

loginForm.appendChild(passwordInput);

loginForm.action = '/users/auth/password';
}
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class User < ApplicationRecord
devise :timeoutable
devise :timeoutable, :omniauthable
end
10 changes: 9 additions & 1 deletion app/views/application/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<%= javascript_pack_tag 'login', 'data-turbolinks-track': 'reload' %>

<div class="container">
<div class="main-content">
<div class="main-content login">
<h1>Welcome!</h1>
<% flash.each do |name, msg| %>
<% if msg.is_a?(String) %>
Expand All @@ -8,5 +10,11 @@
</div>
<% end %>
<% end %>

<%= form_with url: user_idp_discovery_url, local: false, method: 'post', class: 'login-form', id: 'login-form' do |form| %>
<%= form.label :email, "Email" %>
<%= form.email_field :email %>
<%= form.submit "Sign in with SAML SSO" %>
<% end %>
</div>
</div>
2 changes: 1 addition & 1 deletion config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
LoBR8TSHJK0BeDFOdfU7tlvB4G3THK2eve5Dm9vs6n2T8c72co5FJl4htY4fe8ruCh4SDwy7SiQY5IxIRPKtVdrepfX8ngUi6dmvAeG9PCdL+ED10FLYgH85d8jX96aguFcd1aOMNMpSP2S0fynJRz/jN1LzkXdBamZLkXJRMTpboiQhQkjGT8XxcoHKuPUpUIU4eqLiZKU5gnEumiUOI1EGED6i96vb5dbZeLn/FFLGWydsPdaXQ1/ezLuQcBlDNNT6ktU9Vonh0htec4FjAeE0OgdMEp1j1wIUgXqczKKE62JgYG4wOHxdLQfD5xcAfgUz3tyYMx9c/8FmICU+BC/1+rwJEqUkRKpQ5Beth88qVr0mlVj+AKwuTPjx+ttDYpOxu3F3N8DUWZBHtxs7vNOHIucRiUE+aiNu--4BKS5tyJINb9izXo--cuhyNutIIr4Mk49ha/IlrA==
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--P+tquRNCUw861tqr--QB47lA/lvQ80hN0R0n4q1g==
2 changes: 2 additions & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'

config.omniauth :saml

# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
Expand Down
38 changes: 38 additions & 0 deletions config/initializers/omniauth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
SAML_SETTINGS = {
'08909493-cc6f-4a67-9986-f8f4452ba1d4' => {
idp_sso_target_url: "https://idp.ossoapp.com/saml-login",
idp_cert: Rails.application.credentials.idp_cert,
},
'4eaff58f-40a2-4ebe-b746-a9dbe2103864' => {
idp_sso_target_url: "https://dev-634049.okta.com/app/dev-634049_railsdemo_1/exk1yj3meeGRUVVT04x7/sso/saml",
idp_cert: Rails.application.credentials.okta_cert,
}
}

UUID_REGEXP = /[0-9a-f]{8}-[0-9a-f]{3,4}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{12}/. freeze

Rails.application.config.middleware.use OmniAuth::Builder do
OmniAuth::MultiProvider.register(
self,
provider_name: :saml,
identity_provider_id_regex: UUID_REGEXP,
path_prefix: '/users/auth/saml',
callback_suffix: 'callback',
) do |identity_provider_id, rack_env|
request = Rack::Request.new(rack_env)

SAML_SETTINGS[identity_provider_id].merge({
assertion_consumer_service_url: acs_url(request.url),
sp_entity_id: sp_entity_id(identity_provider_id, request),
})
end

def acs_url(request_url)
url = request_url.chomp('/callback')
url + '/callback'
end

def sp_entity_id(identity_provider_id, request)
['http:/', request.host_with_port, identity_provider_id].join('/')
end
end
17 changes: 16 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
Rails.application.routes.draw do
devise_for :users
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html

root to: "application#index"
get 'logged_in', to: "application#logged_in"

devise_scope :user do
post '/users/auth/saml_idp',
to: 'omniauth_callbacks#idp_login',
as: 'user_idp_discovery'

match '/users/auth/saml/:identity_provider_id/callback',
via: [:get, :post],
to: 'omniauth_callbacks#saml',
as: 'user_omniauth_callback'

post '/users/auth/saml/:identity_provider_id',
to: 'omniauth_callbacks#passthru',
as: 'user_omniauth_authorize'
end
end