Skip to content

Commit 71960a2

Browse files
authored
Merge pull request #1242 from ElixirTeSS/cross-space-auth
Cross-space authentication
2 parents ce261e3 + 1478a66 commit 71960a2

21 files changed

Lines changed: 374 additions & 17 deletions

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ gem 'rails', '7.2.2.2'
66

77
gem 'active_model_serializers'
88
gem 'activerecord-session_store'
9+
gem 'addressable'
910
gem 'ahoy_matey'
1011
gem 'auto_strip_attributes'
1112
gem 'bootsnap', require: false

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ PLATFORMS
825825
DEPENDENCIES
826826
active_model_serializers
827827
activerecord-session_store
828+
addressable
828829
ahoy_matey
829830
auto_strip_attributes
830831
better_errors

app/controllers/callbacks_controller.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# The controller for callback actions
22
class CallbacksController < Devise::OmniauthCallbacksController
3+
include SpaceRedirect
34

45
Devise.omniauth_configs.each do |provider, config|
56
define_method(provider) do
@@ -11,6 +12,9 @@ class CallbacksController < Devise::OmniauthCallbacksController
1112

1213
def handle_callback(provider, config)
1314
@user = User.from_omniauth(request.env["omniauth.auth"])
15+
if request.env['omniauth.params'] && request.env['omniauth.params']['space_id']
16+
space = Space.find_by_id(request.env['omniauth.params']['space_id'])
17+
end
1418

1519
if @user.new_record?
1620
# new user
@@ -27,14 +31,15 @@ def handle_callback(provider, config)
2731

2832
sign_in @user
2933
flash[:notice] = "#{I18n.t('devise.registrations.signed_up')} Please ensure your profile is correct."
30-
redirect_to edit_user_path(@user)
34+
redirect_to_space(edit_user_path(@user), space)
3135
rescue Exception => e
3236
flash[:notice] = "Login failed: #{e.message.to_s}"
33-
redirect_to new_user_session_path
37+
redirect_to_space(new_user_session_path, space)
3438
end
3539
else
36-
sign_in_and_redirect @user
40+
scope = Devise::Mapping.find_scope!(@user)
41+
sign_in(scope, resource, {})
42+
redirect_to_space(after_sign_in_path_for(@user), space)
3743
end
3844
end
39-
4045
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module SpaceRedirect
2+
extend ActiveSupport::Concern
3+
4+
private
5+
6+
def redirect_to_space(path, space)
7+
if space&.is_subdomain?
8+
port_part = ''
9+
port_part = ":#{request.port}" if (request.protocol == "http://" && request.port != 80) ||
10+
(request.protocol == "https://" && request.port != 443)
11+
redirect_to URI.join("#{request.protocol}#{space.host}#{port_part}", path).to_s, allow_other_host: true
12+
else
13+
redirect_to path
14+
end
15+
end
16+
end

app/controllers/orcid_controller.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
class OrcidController < ApplicationController
2+
include SpaceRedirect
3+
24
before_action :orcid_auth_enabled
35
before_action :authenticate_user!
46
before_action :set_oauth_client, only: [:authenticate, :callback]
@@ -9,12 +11,17 @@ class OrcidController < ApplicationController
911
end
1012

1113
def authenticate
12-
redirect_to @oauth2_client.authorization_uri(scope: '/authenticate'), allow_other_host: true
14+
params = Space.current_space&.default? ? {} : { state: "space_id:#{Space.current_space.id}" }
15+
redirect_to @oauth2_client.authorization_uri(scope: '/authenticate', **params), allow_other_host: true
1316
end
1417

1518
def callback
1619
@oauth2_client.authorization_code = params[:code]
1720
token = Rack::OAuth2::AccessToken::Bearer.new(access_token: @oauth2_client.access_token!)
21+
if params[:state].present?
22+
m = params[:state].match(/space_id:(\d+)/)
23+
space = Space.find_by_id(m[1]) if m
24+
end
1825
orcid = token.access_token&.raw_attributes['orcid']
1926
respond_to do |format|
2027
profile = current_user.profile
@@ -27,7 +34,7 @@ def callback
2734
else
2835
flash[:error] = t('orcid.authentication_failure')
2936
end
30-
format.html { redirect_to current_user }
37+
format.html { redirect_to_space(user_path(current_user), space) }
3138
end
3239
end
3340

@@ -38,7 +45,7 @@ def set_oauth_client
3845
@oauth2_client ||= Rack::OAuth2::Client.new(
3946
identifier: config[:client_id],
4047
secret: config[:secret],
41-
redirect_uri: config[:redirect_uri].presence || orcid_callback_url,
48+
redirect_uri: config[:redirect_uri].presence || orcid_callback_url(host: TeSS::Config.base_uri.host),
4249
authorization_endpoint: '/oauth/authorize',
4350
token_endpoint: '/oauth/token',
4451
host: config[:host].presence || (Rails.env.production? ? 'orcid.org' : 'sandbox.orcid.org')
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class TessDevise::SessionsController < Devise::SessionsController
2+
3+
def create
4+
clear_legacy_cookie
5+
6+
super
7+
end
8+
9+
def destroy
10+
super
11+
12+
clear_legacy_cookie
13+
end
14+
15+
private
16+
17+
def clear_legacy_cookie
18+
# Clean up legacy host-only session cookie
19+
key = Rails.application.config.session_options[:key]
20+
append_set_cookie("#{key}=; path=/; Max-Age=0; HttpOnly; SameSite=Lax")
21+
end
22+
23+
def append_set_cookie(value)
24+
existing = response.headers['Set-Cookie']
25+
26+
case existing
27+
when nil
28+
response.headers['Set-Cookie'] = value
29+
when String
30+
response.headers['Set-Cookie'] = [existing, value]
31+
when Array
32+
existing << value
33+
end
34+
end
35+
end

app/helpers/application_helper.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,11 +700,12 @@ def theme_path
700700
end
701701

702702
def omniauth_login_link(provider, config)
703+
params = Space.current_space&.default? ? {} : { space_id: Space.current_space.id }
703704
link_to(
704705
t('authentication.omniauth.log_in_with',
705706
provider: config.options[:label] ||
706707
t("authentication.omniauth.providers.#{provider}", default: provider.to_s.titleize)),
707-
omniauth_authorize_path('user', provider),
708+
omniauth_authorize_path('user', provider, **params),
708709
method: :post
709710
)
710711
end

app/helpers/spaces_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ def space_feature_options
1111
[t("features.#{f}.short"), f]
1212
end
1313
end
14+
15+
def space_supports_omniauth?(space = current_space)
16+
space.nil? || space.default? || space.is_subdomain?(TeSS::Config.base_uri.domain)
17+
end
1418
end

app/models/space.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class Space < ApplicationRecord
1616
has_many :administrator_roles, -> { where(key: :admin) }, class_name: 'SpaceRole'
1717
has_many :administrators, through: :administrator_roles, source: :user, class_name: 'User'
1818

19+
auto_strip_attributes :title, :description, :host
20+
21+
validates :title, presence: true
22+
validates :host, presence: true, uniqueness: true, format: /\A[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\z/i
1923
validates :theme, inclusion: { in: TeSS::Config.themes.keys, allow_blank: true }
2024
validate :disabled_features_valid?
2125

@@ -65,6 +69,10 @@ def enabled_features
6569
(FEATURES - disabled_features)
6670
end
6771

72+
def is_subdomain?(domain = TeSS::Config.base_uri.domain)
73+
(host == domain || host.ends_with?(".#{domain}"))
74+
end
75+
6876
private
6977

7078
def disabled_features_valid?

app/views/layouts/_login_menu.html.erb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
<strong>Log In</strong> <span class="caret"></span>
1010
</a>
1111
<ul class="dropdown-menu dropdown-menu-right">
12-
<% Devise.omniauth_configs.each do |provider, config| -%>
13-
<li class="dropdown-item"><%= omniauth_login_link(provider, config) %></li>
12+
<% if space_supports_omniauth? %>
13+
<% Devise.omniauth_configs.each do |provider, config| -%>
14+
<li class="dropdown-item"><%= omniauth_login_link(provider, config) %></li>
15+
<% end %>
1416
<% end %>
1517

1618
<li class="dropdown-item">

0 commit comments

Comments
 (0)