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

OIDC #1396

Closed
wants to merge 7 commits into from
Closed

OIDC #1396

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: 2 additions & 1 deletion BrainPortal/app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def new #:nodoc:
@browser_version = ua.browser_version || "(unknown browser version)"

@globus_uri = globus_login_uri(globus_url) # can be nil

@oidc_client = (RemoteResource.current_resource.meta[:oidc_client] || "Globus").capitalize

respond_to do |format|
format.html
format.any { head :unauthorized }
Expand Down
3 changes: 2 additions & 1 deletion BrainPortal/app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def index #:nodoc:
# GET /user/1.xml
# GET /user/1.json
def show #:nodoc:
@user = User.find(params[:id])
@user = User.find(params[:id])
@oidc_client = (RemoteResource.current_resource.meta[:oidc_client] || "Globus").capitalize

cb_error "You don't have permission to view this user.", :redirect => start_page_path unless edit_permission?(@user)

Expand Down
4 changes: 2 additions & 2 deletions BrainPortal/app/views/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@
<strong>OR</strong>
</td>
<td class="field">
<%= link_to 'Sign In With Globus', @globus_uri, :class => 'button globus_button' %>
<%= link_to "Sign In With #{@oidc_client}", @globus_uri, :class => 'button globus_button' %>
<div class="field_explanation">
(Only available if you have already linked your<br>
CBRAIN account to a Globus identity)
CBRAIN account to a <%= @oidc_client %> identity)
</div>
</td>
</tr>
Expand Down
14 changes: 7 additions & 7 deletions BrainPortal/app/views/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@
</div>
<% end %>

<% t.edit_cell('meta[allowed_globus_provider_names]', :header => 'Forced Globus Identity Providers', :content => @user.meta['allowed_globus_provider_names'] || '') do %>
<% t.edit_cell('meta[allowed_globus_provider_names]', :header => "Forced #{@oidc_client} Identity Providers", :content => @user.meta['allowed_globus_provider_names'] || '') do %>
<%= text_field_tag "meta[allowed_globus_provider_names]", @user.meta['allowed_globus_provider_names'], :size => 40 %><br />
<div class="field_explanation">
If set, must be a list of Globus identity provider names separated by commas. A single '*' is
If set, must be a list of <%= @oidc_client %> identity provider names separated by commas. A single '*' is
also allowed to mean any provider name.
</div>
<% end %>
Expand Down Expand Up @@ -172,22 +172,22 @@
%>

<% if @globus_uri %>
<% t.cell "Globus Provider", :show_width => 2 do %>
<% t.cell "#{@oidc_client} Provider", :show_width => 2 do %>
<% if prov_id %>
<strong>Provider name:</strong> <%= prov_name %><br>
<strong>Provider user:</strong> <%= prov_user %><br>
<% if @user.id == current_user.id # show button on user own page, hide on other users pages %>
<%= link_to('Unlink this Globus identity', unlink_globus_path,
<%= link_to("Unlink this #{@oidc_client} identity", unlink_globus_path,
:class => "button",
:method => :post,
:data => { :confirm => "Are you sure you want to unlink your account with this Globus identity?" }
:data => { :confirm => "Are you sure you want to unlink your account with this #{@oidc_client} identity?" }
)
%>
<% end %>
<% else %>
(No Globus identity linked to your account)<br>
(No <%= @oidc_client %> identity linked to your account)<br>
<% if @user.id == current_user.id # Globus button works only on user own account %>
<%= link_to 'Link a Globus identity', @globus_uri, :class => 'button globus_button' %>
<%= link_to "Link a #{@oidc_client} identity", @globus_uri, :class => 'button globus_button' %>
<% end %>
<% end %>
<% end %>
Expand Down
85 changes: 48 additions & 37 deletions BrainPortal/lib/globus_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,36 @@ module GlobusHelpers

Revision_info=CbrainFileRevision[__FILE__] #:nodoc:

# GLOBUS authentication URL constants
# OIDC authentication URL constants
# Maybe should be made configurable.
GLOBUS_AUTHORIZE_URI = "https://auth.globus.org/v2/oauth2/authorize" # will be issued a GET with params
GLOBUS_TOKEN_URI = "https://auth.globus.org/v2/oauth2/token" # will be issued a POST with a single code
GLOBUS_LOGOUT_URI = "https://auth.globus.org/v2/web/logout" # for pages that provide this link

# Returns the URI to send users to the GLOBUS authentication page.
# will be issued a GET with params
GLOBUS_AUTHORIZE_URI = RemoteResource.current_resource.meta[:oidc_authorize_uri] || "https://auth.globus.org/v2/oauth2/authorize"
# will be issued a POST with a single code
GLOBUS_TOKEN_URI = RemoteResource.current_resource.meta[:oidc_token_uri] || "https://auth.globus.org/v2/oauth2/token"
# for pages that provide this link
GLOBUS_LOGOUT_URI = RemoteResource.current_resource.meta[:oidc_logout_uri] || "https://auth.globus.org/v2/web/logout"


# Define OIDC scope
$scope,$oidc_name = (RemoteResource.current_resource.meta[:oidc_client] || "").casecmp?("keycloak") ?
["openid email profile", "Keycloak"] :
["urn:globus:auth:scope:auth.globus.org:view_identities openid email profile", "Globus"]


# Returns the URI to send users to the OIDC authentication page.
# The parameter globus_action_url should be the URL to the controller
# action here in CBRAIN that will received the POST response.
def globus_login_uri(globus_action_url)
return nil if api_request?
return nil unless globus_auth_configured?

# Create the URI to authenticate with GLOBUS
# Create the URI to authenticate with OIDC
globus_params = {
:client_id => globus_client_id,
:response_type => 'code',
:scope => "urn:globus:auth:scope:auth.globus.org:view_identities openid email profile",
:redirect_uri => globus_action_url, # generated from Rails routes
:scope => $scope,
:redirect_uri => globus_action_url, # generated from Rails routes
:state => globus_current_state, # method is below
}
GLOBUS_AUTHORIZE_URI + '?' + globus_params.to_query
Expand All @@ -54,9 +65,9 @@ def globus_logout_uri
end

def globus_fetch_token(code, globus_action_url)
# Query Globus; this returns all the info we need at the same time.
# Query OIDC; this returns all the info we need at the same time.
auth_header = globus_basic_auth_header # method is below
response = Typhoeus.post(GLOBUS_TOKEN_URI,
response = Typhoeus.post(GLOBUS_TOKEN_URI,
:body => {
:code => code,
:redirect_uri => globus_action_url,
Expand All @@ -75,7 +86,7 @@ def globus_fetch_token(code, globus_action_url)

return identity_struct
rescue => ex
Rails.logger.error "GLOBUS token request failed: #{ex.class} #{ex.message}"
Rails.logger.error "#{$oidc_name} token request failed: #{ex.class} #{ex.message}"
return nil
end

Expand All @@ -90,22 +101,22 @@ def globus_basic_auth_header
end

# Returns a string that should stay constants during the entire
# globus negotiations. The current Rails session_id, encoded, will do
# OIDC negotiations. The current Rails session_id, encoded, will do
# the trick. The Rails session is maintained by a cookie already
# created and maintained, at this point.
def globus_current_state
Digest::MD5.hexdigest( request.session_options[:id] )
end

# Return the registered globus endpoint client ID.
# Return the registered OIDC endpoint client ID.
# This value must be configured by the CBRAIN admin
# in the meta data of the portal. Returns nil if unset.
def globus_client_id
myself = RemoteResource.current_resource
myself.meta[:globus_client_id].presence.try(:strip)
end

# Return the registered globus endpoint client secret.
# Return the registered OIDC endpoint client secret.
# This value must be configured by the CBRAIN admin
# in the meta data of the portal. Returns nil if unset.
def globus_client_secret
Expand All @@ -114,7 +125,7 @@ def globus_client_secret
end

# Returns true if the CBRAIN system is configured for
# globus auth.
# OIDC auth.
def globus_auth_configured?
myself = RemoteResource.current_resource
site_uri = myself.site_url_prefix.presence
Expand All @@ -125,43 +136,43 @@ def globus_auth_configured?
true
end

# Record the globus identity for the current user.
# Record the OIDC identity for the current user.
# (This maybe should be made into a User model method)
def record_globus_identity(user, globus_identity)

# In the case where a user must auth with a specific set of
# globus providers, we find the first identity that
# OIDC providers, we find the first identity that
# matches a name of that set.
identity = set_of_identities(globus_identity).detect do |idstruct|
user_can_link_to_globus_identity?(user, idstruct)
end

provider_id = identity['identity_provider'] || cb_error("Globus: No identity provider")
provider_name = identity['identity_provider_display_name'] || cb_error("Globus: No identity provider name")
provider_id = identity['identity_provider'] || identity['aud'] || cb_error("#{$oidc_name}: No identity provider")
provider_name = identity['identity_provider_display_name'] || identity['azp'] || cb_error("#{$oidc_name}: No identity provider name")
pref_username = identity['preferred_username'] ||
identity['username'] || cb_error("Globus: No preferred username")
identity['username'] || cb_error("#{$oidc_name}: No preferred username")

# Special case for ORCID, because we already have fields for that provider
# We do NOT do this in the case where the user is forced to auth with globus.
# We do NOT do this in the case where the user is forced to auth with OIDC.
if provider_name == 'ORCID' && ! user_must_link_to_globus?(user)
orcid = pref_username.sub(/@.*/, "")
user.meta['orcid'] = orcid
user.addlog("Linked to ORCID identity: '#{orcid}' through Globus")
user.addlog("Linked to ORCID identity: '#{orcid}' through #{$oidc_name}")
return
end

user.meta[:globus_provider_id] = provider_id
user.meta[:globus_provider_name] = provider_name # used in show page
user.meta[:globus_preferred_username] = pref_username
user.addlog("Linked to Globus identity: '#{pref_username}' on provider '#{provider_name}'")
user.addlog("Linked to #{$oidc_name} identity: '#{pref_username}' on provider '#{provider_name}'")
end

# Removes the recorded globus identity for +user+
# Removes the recorded OIDC identity for +user+
def unlink_globus_identity(user)
user.meta[:globus_provider_id] = nil
user.meta[:globus_provider_name] = nil
user.meta[:globus_preferred_username] = nil
user.addlog("Unlinked Globus identity")
user.addlog("Unlinked #{$oidc_name} identity")
end

def set_of_identities(globus_identity)
Expand Down Expand Up @@ -201,19 +212,19 @@ def user_must_link_to_globus?(user)
end

def wipe_user_password_after_globus_link(user)
user.update_attribute(:crypted_password, 'Wiped-By-Globus-Link-' + User.random_string)
user.update_attribute(:salt , 'Wiped-By-Globus-Link-' + User.random_string)
user.update_attribute(:crypted_password, "Wiped-By-#{$oidc_name}-Link-" + User.random_string)
user.update_attribute(:salt , "Wiped-By-#{$oidc_name}-Link-" + User.random_string)
user.update_attribute(:password_reset , false)
end

# Given a globus identity structure, find the user that matches it.
# Given a OIDC identity structure, find the user that matches it.
# Returns the user object if found; returns a string error message otherwise.
def find_user_with_globus_identity(globus_identity)

provider_name = globus_identity['identity_provider_display_name']
pref_username = globus_identity['preferred_username'] || globus_identity['username']

id_set = set_of_identities(globus_identity) # a globus record can contain several identities
id_set = set_of_identities(globus_identity) # an OIDC record can contain several identities

# For each present identity, find all users that have it.
# We only allow ONE cbrain user to link to any of the identities.
Expand All @@ -222,14 +233,14 @@ def find_user_with_globus_identity(globus_identity)
end

if users.size == 0
Rails.logger.error "GLOBUS warning: no CBRAIN accounts found for identity '#{pref_username}' on provider '#{provider_name}'"
return "No CBRAIN user matches your Globus identity. Create a CBRAIN account or link your existing CBRAIN account to your Globus provider."
Rails.logger.error "#{$oidc_name.upcase} warning: no CBRAIN accounts found for identity '#{pref_username}' on provider '#{provider_name}'"
return "No CBRAIN user matches your #{$oidc_name} identity. Create a CBRAIN account or link your existing CBRAIN account to your #{$oidc_name} provider."
end

if users.size > 1
loginnames = users.map(&:login).join(", ")
Rails.logger.error "GLOBUS error: multiple CBRAIN accounts (#{loginnames}) found for identity '#{pref_username}' on provider '#{provider_name}'"
return "Several CBRAIN user accounts match your Globus identity. Please contact the CBRAIN admins."
Rails.logger.error "#{$oidc_name.upcase} error: multiple CBRAIN accounts (#{loginnames}) found for identity '#{pref_username}' on provider '#{provider_name}'"
return "Several CBRAIN user accounts match your #{$oidc_name} identity. Please contact the CBRAIN admins."
end

# The one lucky user
Expand All @@ -241,17 +252,17 @@ def find_user_with_globus_identity(globus_identity)
# be empty (no such users) or contain more than one
# user (an account management error).
def find_users_with_specific_identity(identity)
provider_id = identity['identity_provider'] || cb_error("Globus: No identity provider")
provider_name = identity['identity_provider_display_name'] || cb_error("Globus: No identity provider name")
provider_id = identity['identity_provider'] || identity['aud'] || cb_error("#{$oidc_name}: No identity provider")
provider_name = identity['identity_provider_display_name'] || identity['azp'] || cb_error("#{$oidc_name}: No identity provider name")
pref_username = identity['preferred_username'] ||
identity['username'] || cb_error("Globus: No preferred username")
identity['username'] || cb_error("#{$oidc_name}: No preferred username")

# Special case for ORCID, because we already have fields for that provider
if provider_name == 'ORCID'
orcid = pref_username.sub(/@.*/, "")
users = User.find_all_by_meta_data(:orcid, orcid).to_a
return users if users.present?
# otherwise we fall through to detect users who linked with ORCID through Globus
# otherwise we fall through to detect users who linked with ORCID through OIDC
end

# All other globus providers
Expand Down
Loading