Skip to content

Commit

Permalink
Merge pull request #2769 from sciencehistory/microsoft_sso
Browse files Browse the repository at this point in the history
Microsoft SSO ( keywords Entra / Azure / OmniAuth / Oauth )
  • Loading branch information
eddierubeiz authored Nov 12, 2024
2 parents e42be55 + a79907b commit 85532fc
Show file tree
Hide file tree
Showing 24 changed files with 622 additions and 102 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ gem 'bootstrap4-kaminari-views'

gem 'devise', "~> 4.5" # user accounts and login
gem 'access-granted', "~> 1.0" # authorization
gem 'omniauth-entra-id'
gem 'omniauth-rails_csrf_protection'


# decorating and truncating html
gem "rinku", '~> 2.0' # auto-linking
Expand Down
25 changes: 20 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,13 @@ GEM
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faraday (2.10.1)
faraday-net_http (>= 2.0, < 3.2)
faraday (2.12.0)
faraday-net_http (>= 2.0, < 3.4)
json
logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-net_http (3.1.1)
faraday-net_http (3.3.0)
net-http
faraday-retry (2.2.1)
faraday (~> 2.0)
Expand Down Expand Up @@ -332,7 +333,7 @@ GEM
activesupport (>= 5.0.0)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.2)
jwt (2.9.3)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
Expand Down Expand Up @@ -448,6 +449,18 @@ GEM
oga (3.4)
ast
ruby-ll (~> 2.1)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-entra-id (3.0.0)
omniauth-oauth2 (~> 1.8)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.0)
Expand Down Expand Up @@ -718,7 +731,7 @@ GEM
aws-sdk-s3 (~> 1.0)
content_disposition (~> 1.0)
roda (>= 2.27, < 4)
uri (0.13.0)
uri (0.13.1)
useragent (0.16.10)
view_component (3.13.0)
activesupport (>= 5.2.0, < 8.0)
Expand Down Expand Up @@ -814,6 +827,8 @@ DEPENDENCIES
net-protocol (!= 0.2.0)
oai (~> 1.0, >= 1.0.1)
observer
omniauth-entra-id
omniauth-rails_csrf_protection
pdf-reader (~> 2.2)
pg (>= 0.18, < 2.0)
prawn (~> 2.2)
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ Configuration for solr_wrapper is at `./.solr_wrapper.yml`

### Account management

We shouldn't have to use account management rake tasks as much, since there is now admin web interface for creating and editing accounts. But they are still there, as they can be convenient for setting up a dev environment or perhaps bootstrapping a production environment with an admin account, or in general automating things involving users.
We shouldn't have to use account management rake tasks as much, since we provide an admin web interface for creating and editing accounts. But they are still there, as they can be convenient for setting up a dev environment or perhaps bootstrapping a production environment with an admin account, or in general automating things involving users.

```shell
./bin/rake scihist:user:create[[email protected]]
Expand All @@ -369,6 +369,11 @@ This can be useful if we need to do some maintenance that doesn't bring down the

This feature was in our v1 sufia-based app, we copied it over.


## Using Microsoft SSO
It's possible to configure the app to use Microsoft single sign-on (SSO) instead of standard email-and-password authentication.
Details are in [a separate README file](config/initializers/MICROSOFT_SSO_README.md).

## Thanks

<img src="https://www.browserstack.com/images/layout/browserstack-logo-600x315.png" width="280"/>
Expand Down
12 changes: 12 additions & 0 deletions app/components/scihist_footer_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
<li><%= link_to "About", about_url, class: "btn btn-brand-main" %></li>
<li><%= link_to "FAQ", faq_url, class: "btn btn-brand-main" %></li>
<li><%= link_to "Contact", contact_url, class: "btn btn-brand-main" %></li>
<% if ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso) %>
<li>
<% if helpers.can? :access_staff_functions %>
<%= link_to "Log out", logout_path, class: 'btn btn-brand-main' %>
<% else %>
<%= link_to "Log in",
user_entra_id_omniauth_authorize_path, method: :post,
class: 'btn btn-brand-main'
%>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ def update
end
end

# POST /users/1
# POST /admin/users/:id/send_password_reset
def send_password_reset
raise "This method should be unreachable." if ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso)
@user.send_reset_password_instructions
redirect_to admin_users_path, notice: "Password reset email sent to #{@user.email}"
end
Expand Down
17 changes: 10 additions & 7 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ class ApplicationController < ActionController::Base


rescue_from "AccessGranted::AccessDenied" do |exception|
redirect_path = if current_user.present?
root_path
else
new_user_session_path
end

redirect_to redirect_path, alert: "You don't have permission to access requested page."
redirect_to root_path, alert: "You don't have permission to access that page."
end

around_action :batch_kithe_indexable
Expand All @@ -50,4 +44,13 @@ def show_ie_unsupported_warning?
browser.ie? && !cookies[:ieWarnDismiss]
end
helper_method :show_ie_unsupported_warning?

# Since we're displaying the log in link on every page,
# after the user logs in successfully, let's redirect to
# wherever they were before they clicked the log in link.
# See https://www.rubydoc.info/github/plataformatec/devise/Devise/Controllers/Helpers
def after_sign_in_path_for(resource)
stored_location_for(resource) || request.env['omniauth.origin'] || super
end

end
70 changes: 70 additions & 0 deletions app/controllers/auth_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# This controller provides methods used to authenticate a user using
# Microsoft Single Sign On / Entra / Azure.
# Links to more documentation are at config/initializers/devise.rb.
#
# Note that if ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso) is not set to true,
# this *entire* controller is turned off in config/routes.rb .
# (See devise_for :users in that file.)
class AuthController < Devise::OmniauthCallbacksController

before_action :maybe_redirect_back, only: [:passthru, :entra_id]

# This method signs a user in after they authenticate with Microsoft SSO.
def entra_id
email = request.env['omniauth.auth']['info']['email']
@user = User.where('email ILIKE ?', "%#{ User.sanitize_sql_like(email) }%").first

unless @user&.persisted?
flash[:alert] = "You can't currently log in to the Digital Collections. Please contact a Digital Collections administrator."
redirect_back(fallback_location: root_path)
return
end

if @user.locked_out?
flash[:alert] = "Sorry, this user is not allowed to log in."
redirect_back(fallback_location: root_path)
return
end

flash[:notice] = 'Signed in successfully.'
sign_in_and_redirect @user, event: :authentication
end

# Log a user out of the digital collections,
# *then* log them out of Microsoft SSO.
def sso_logout
# There should not be a route to this method unless ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso).
raise "This method should be unreachable." unless ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso)
sign_out current_user
redirect_to sso_logout_path, allow_other_host: true
end

private

# We need to provide a default path for newly signed-in users.
# Usual login paths do not call this method, but when
# the SSO setup is misconfigured,
# this method does sometimes get called, resulting in a 500 error.
# Instead, we send users to the root path.
def new_session_path *args
flash[:notice] = "This URL is not meant for regular users."
root_path
end

def sso_logout_path
@logout_path ||= OmniAuth::Strategies::EntraId::BASE_URL +
"/common/oauth2/v2.0/logout" +
"?post_logout_redirect_uri=" +
ScihistDigicoll::Env.lookup(:app_url_base) +
root_path
end

def maybe_redirect_back
unless ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso)
flash[:alert] = "Sorry, you can't log in this way."
redirect_back(fallback_location: root_path)
return
end
end

end
13 changes: 13 additions & 0 deletions app/controllers/passwords_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# To minimize confusion, let's make password manipulation unavailable if we're currently managing
# auth using Microsoft. These passwords are irrelevant and will just cause confusion, since the user likely
# has a totally different password in Entra.
class PasswordsController < Devise::PasswordsController
before_action :maybe_redirect_back
private
def maybe_redirect_back
return unless ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso)
flash[:alert] = "Passwords are managed in Microsoft SSO now."
redirect_back(fallback_location: root_path)
return
end
end
9 changes: 6 additions & 3 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ class User < ApplicationRecord
#
# jrochkind did:
# * comment out registerable, we don't allow registrations

devise :database_authenticatable,
:recoverable, :rememberable, :validatable
:recoverable,
:rememberable,
:validatable,
:omniauthable,
omniauth_providers: %i[entra_id]

has_many :cart_items, dependent: :delete_all
has_many :works_in_cart, through: :cart_items, source: :work

# This will correspond to a "role" in the AccessPolicy class.
# "editor" will replace the current "staff" role.
# A new "reader" type will be added in a future PR.
USER_TYPES = %w{admin editor staff_viewer}.freeze
enum :user_type, USER_TYPES.collect {|v| [v, v]}.to_h
validates :user_type, presence: true
Expand Down
4 changes: 3 additions & 1 deletion app/views/admin/users/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
</td>
<td>
<%= link_to 'Edit', edit_admin_user_path(user), class: "btn btn-sm btn-outline-secondary" %>
<%= link_to 'Send password reset', send_password_reset_admin_user_path(user), method: "post", class: "btn btn-sm btn-outline-secondary" %>
<% unless ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso)%>
<%= link_to 'Send password reset', send_password_reset_admin_user_path(user), method: "post", class: "btn btn-sm btn-outline-secondary" %>
<% end %>
</td>
</tr>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/application/_front_end_admin_navbar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</li>

<li class="nav-item">
<%= link_to("Logout", destroy_user_session_path, method: :delete, class: "nav-link") %>
<%= link_to("Logout", logout_path, class: "nav-link") %>
</li>
</ul>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/views/layouts/admin.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@
<li><%= link_to "Orphan Report", admin_orphan_report_path %></li>
<li><hr></li>
<% if current_user %>
<li><%= link_to("Logout", destroy_user_session_path, method: :delete) %></li>
<li><%= link_to "Logout", logout_path %></li>
<% else %>
<li><%= link_to('Login', new_user_session_path) %></li>
<li><%= link_to 'Login', new_user_session_path %></li>
<% end %>
</ul>
</div>
Expand Down
21 changes: 21 additions & 0 deletions config/initializers/MICROSOFT_SSO_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Microsoft SSO
By default, when you install the app, users log in using a combination of a username and password.

If you want, though, you can try using Microsoft’s Entra to authenticate users instead.
- [This PR](https://github.com/sciencehistory/scihist_digicoll/pull/2769) has a lot of details and context.
- Authentication is provided by two gems, `omniauth-entra-id` and `omniauth-rails_csrf_protection`.
- A feature switch, ENV setting `:log_in_using_microsoft_sso`, determines whether the app uses Microsoft SSO to sign in or not. This is turned off by default, so if you want to use it in Dev, you will need to add some env variables (see “using SSO in dev”).
- The Microsoft auth provider is configured with three more ENV settings:
- :microsoft_sso_client_id identifies the app to Microsoft SSO;
- :microsoft_sso_client_secret authenticates the app to Microsoft SSO;
- :microsoft_sso_tenant_id identifies the Microsoft SSO directory the app wants to check (namely the Institute one. This ID is the same for dev, staging and prod.)
- Most of the configuration is done in two files:
- config/initializers/devise.rb
- config/routes.rb

## Using SSO in a development environment
Single sign-on is turned off by default in development. If you want to try using SSO in development, you can temporarily add something like the following to your local_env.yml file:
- log_in_using_microsoft_sso: true
- microsoft_sso_client_id: [...]
- microsoft_sso_client_secret: [...]
- microsoft_sso_tenant_id: [...]
58 changes: 54 additions & 4 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,60 @@
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete

# ==> 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'
####
# START MICROSOFT SINGLE SIGN ON / SSO / ENTRA / AZURE / OAUTH / OMNIAUTH
# More details about how we use Microsoft SSO (aka Entra, aka Azure) are in the wiki.
#
# See also https://github.com/sciencehistory/scihist_digicoll/pull/2769
# See also config/initializers/devise.rb
# See also the https://sciencehistory.atlassian.net/wiki/spaces/HDC/pages/1915748368/Heroku+Operational+Components+Overview#Microsoft-SSO
# See also https://portal.azure.com/
# See also 1Password (or equivalent password store).
#

# If you turn on :log_in_using_microsoft_sso ...
if ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso)
# ... we will refuse to turn on Microsoft SSO, or even start the app,
# unless we have all the config variables we need.
ready_to_configure_microsoft_sso = [
ScihistDigicoll::Env.lookup(:microsoft_sso_tenant_id ).present?,
ScihistDigicoll::Env.lookup(:microsoft_sso_client_id ).present?,
ScihistDigicoll::Env.lookup(:microsoft_sso_client_secret).present?
].all?
unless ready_to_configure_microsoft_sso
raise "Setting log_in_using_microsoft_sso is set to true, but we are missing some values we need to configure Microsoft SSO."
end
end

# Devise's configuration options cannot be reloaded on the fly:
# see https://github.com/heartcombo/devise?tab=readme-ov-file#getting-started .
# However, we still need to maintain tests of *both* authentication workflows,
# both with and without Microsoft SSO.
# This means that we still need to configure Microsoft SSO here for testing purposes,
# even if ScihistDigicoll::Env.lookup(:log_in_using_microsoft_sso) happens to be false.
config.omniauth(
:entra_id,
{

# "Tenant" or "Directory" is what Microsoft calls our Entra directory.
# This will have the same value for *all* applications (digital collections or other)
# that authenticate using Microsoft SSO.
tenant_id: ScihistDigicoll::Env.lookup(:microsoft_sso_tenant_id),

# "Client" or "Application" is what Microsoft calls a website that uses Microsoft SSO.
# This will have a different value for dev; staging; and production.
client_id: ScihistDigicoll::Env.lookup(:microsoft_sso_client_id),

# This is effectively a password - a shared secret.
client_secret: ScihistDigicoll::Env.lookup(:microsoft_sso_client_secret),

}
)

#
# END MICROSOFT SINGLE SIGN ON / SSO / ENTRA / AZURE / OAUTH / OMNIAUTH
####


# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
Expand Down
Loading

0 comments on commit 85532fc

Please sign in to comment.