Skip to content
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ jobs:
MEMCACHE_SERVERS: localhost:${{ job.services.memcached.ports[11211] }}
REDIS_URL: redis://localhost:${{ job.services.redis.ports[6379] }}
ORCID_CLIENT_ID: ${{ secrets.ORCID_CLIENT_ID_FOR_TESTING }}
ORCID_AUTO_UPDATE_CLIENT_ID: ${{ secrets.ORCID_CLIENT_ID_FOR_TESTING }}
ORCID_SEARCH_AND_LINK_CLIENT_ID: ${{ secrets.ORCID_CLIENT_ID_FOR_TESTING }}
ORCID_TOKEN: ${{ secrets.ORCID_TOKEN_FOR_TESTING }}
run: |
bundle exec rubocop
Expand Down
16 changes: 1 addition & 15 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ def globus

def orcid
auth = request.env["omniauth.auth"]
omni_params = request.env["omniauth.params"]
omniauth = flash[:omniauth] || {}

if current_user.present?
Expand All @@ -127,13 +126,6 @@ def orcid
if @user.persisted?
sign_in @user

# Refresh ORCID token if the flag isn't explicitly false
if omni_params["fetch_token"] != "false"
@user.update(orcid_expires_at: User.timestamp(auth.credentials),
orcid_token: auth.credentials.token)
flash[:notice] = "ORCID token successfully refreshed."
end

cookies[:_datacite] = encode_cookie(@user.jwt)

if stored_location_for(:user) == ENV["BLOG_URL"] + "/admin/"
Expand All @@ -150,13 +142,7 @@ def orcid

netlify_response(token: token, content: content)
else

# Redirect to Commons if the flag isn't explicitly false. Otherwise redirect to profile settings
if omni_params["redirect_to_commons"] != "false"
redirect_to "#{ENV['COMMONS_URL']}/orcid.org/#{current_user.orcid}"
else
redirect_to stored_location_for(:user) || setting_path("me")
end
redirect_to stored_location_for(:user) || setting_path("me")
end
else
flash[:alert] = @user.errors.map { |k, v| "#{k}: #{v}" }.join("<br />").html_safe || "Error signing in with #{provider}"
Expand Down
213 changes: 213 additions & 0 deletions app/controllers/users/orcid_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# frozen_string_literal: true

require "faraday"
require "json"

module Users
class OrcidController < ApplicationController
BASE_URL = "#{ENV["ORCID_URL"]}/oauth"
SCOPES_AUTO_UPDATE = ["/activities/update", "/read-limited"].freeze
SCOPES_SEARCH_AND_LINK = ["/activities/update", "/read-limited"].freeze

before_action :load_user, only: [:auto_update_refresh, :auto_update_revoke, :search_and_link_refresh, :search_and_link_revoke]

# Auto-update methods
def auto_update_auth
auth_url = build_auth_url(ENV["ORCID_AUTO_UPDATE_CLIENT_ID"],
ENV["ORCID_AUTO_UPDATE_REDIRECT_URI"],
SCOPES_AUTO_UPDATE.join(" "))

redirect_to auth_url
end


def auto_update_callback
response = callback(ENV["ORCID_AUTO_UPDATE_CLIENT_ID"],
ENV["ORCID_AUTO_UPDATE_CLIENT_SECRET"],
params[:code])

@user = User.from_orcid(response[:id])

@user.update(orcid_auto_update_access_token: response[:access_token],
orcid_auto_update_refresh_token: response[:refresh_token],
orcid_auto_update_expires_at: response[:expires_at])

redirect_to stored_location_for(:user) || setting_path("me")
end


def auto_update_refresh
response = refresh(ENV["ORCID_AUTO_UPDATE_CLIENT_ID"],
ENV["ORCID_AUTO_UPDATE_CLIENT_SECRET"],
@user.orcid_auto_update_refresh_token)

@user.update(orcid_auto_update_access_token: response[:access_token],
orcid_auto_update_refresh_token: response[:refresh_token],
orcid_auto_update_expires_at: response[:expires_at])

redirect_to stored_location_for(:user) || setting_path("me")
end


def auto_update_revoke
revoke(ENV["ORCID_AUTO_UPDATE_CLIENT_ID"],
ENV["ORCID_AUTO_UPDATE_CLIENT_SECRET"],
@user.orcid_auto_update_access_token)

@user.update(orcid_auto_update_access_token: nil,
orcid_auto_update_refresh_token: nil,
orcid_auto_update_expires_at: nil)

redirect_to stored_location_for(:user) || setting_path("me")
end


# Search and Link methods
def search_and_link_auth
auth_url = build_auth_url(ENV["ORCID_SEARCH_AND_LINK_CLIENT_ID"],
ENV["ORCID_SEARCH_AND_LINK_REDIRECT_URI"] + "?redirect_to_commons=false",
SCOPES_SEARCH_AND_LINK.join(" "))

redirect_to auth_url
end


def search_and_link_callback
response = callback(ENV["ORCID_SEARCH_AND_LINK_CLIENT_ID"],
ENV["ORCID_SEARCH_AND_LINK_CLIENT_SECRET"],
params[:code])


@user = User.from_orcid(response[:id])

@user.update(orcid_search_and_link_access_token: response[:access_token],
orcid_search_and_link_refresh_token: response[:refresh_token],
orcid_search_and_link_expires_at: response[:expires_at])

# Redirect to Commons if the flag isn't explicitly false. Otherwise redirect to profile settings
if params["redirect_to_commons"] != "false"
redirect_to "#{ENV['COMMONS_URL']}/orcid.org/#{@user.orcid}"
else
redirect_to stored_location_for(:user) || setting_path("me")
end
end


def search_and_link_refresh
response = refresh(ENV["ORCID_SEARCH_AND_LINK_CLIENT_ID"],
ENV["ORCID_SEARCH_AND_LINK_CLIENT_SECRET"],
@user.orcid_search_and_link_refresh_token)

@user.update(orcid_search_and_link_access_token: response[:access_token],
orcid_search_and_link_refresh_token: response[:refresh_token],
orcid_search_and_link_expires_at: response[:expires_at])

redirect_to stored_location_for(:user) || setting_path("me")
end


def search_and_link_revoke
revoke(ENV["ORCID_SEARCH_AND_LINK_CLIENT_ID"],
ENV["ORCID_SEARCH_AND_LINK_CLIENT_SECRET"],
@user.orcid_search_and_link_access_token)

@user.update(orcid_search_and_link_access_token: nil,
orcid_search_and_link_refresh_token: nil,
orcid_search_and_link_expires_at: nil)

redirect_to stored_location_for(:user) || setting_path("me")
end


def load_user
if user_signed_in?
@user = current_user
else
fail CanCan::AccessDenied.new("Please sign in first.", :read, User)
end
end

private
def build_auth_url(client_id, redirect_uri, scope)
response_type = "code"
"#{BASE_URL}/authorize?client_id=#{client_id}&response_type=#{response_type}&scope=#{scope}&redirect_uri=#{redirect_uri}"
end


def callback(client_id, client_secret, code)
conn = Faraday.new(BASE_URL + "/token")

body = {
client_id: client_id,
client_secret: client_secret,
grant_type: "authorization_code",
code: code,
}

response = conn.post do |req|
req.headers["Accept"] = "application/json"
req.body = body
end

unless response.success?
raise "Error exchanging code for tokens: #{response.status} - #{response.body}"
end

parse_body(response.body)
end


def refresh(client_id, client_secret, refresh_token)
conn = Faraday.new(BASE_URL + "/token")

body = {
client_id: client_id,
client_secret: client_secret,
refresh_token: refresh_token,
grant_type: "refresh_token",
}

response = conn.post do |req|
req.body = body
end

unless response.success?
raise "Error refreshing tokens: #{response.status} - #{response.body}"
end

parse_body(response.body)
end


def revoke(client_id, client_secret, access_token)
conn = Faraday.new(BASE_URL + "/revoke")

body = {
client_id: client_id,
client_secret: client_secret,
token: access_token,
}

response = conn.post do |req|
req.body = body
end

unless response.success?
raise "Error revoking tokens: #{response.status} - #{response.body}"
end
end


def parse_body(json)
body = JSON.parse(json)
expires_at = Time.now.utc + body["expires_in"].seconds

{
id: body["orcid"],
access_token: body["access_token"],
refresh_token: body["refresh_token"],
expires_at: expires_at
}
end
end
end
11 changes: 10 additions & 1 deletion app/models/claim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def collect_data(options = {})
return OpenStruct.new(body: { "errors" => [{ "title" => "Missing data" }] }) if work.data.nil?

# orcid_token has expired, but is not default 1970-01-01
return OpenStruct.new(body: { "errors" => [{ "status" => 401, "title" => "token has expired." }] }) if (Date.new(1970, 1, 2).beginning_of_day..Date.today.end_of_day) === user.orcid_expires_at
return OpenStruct.new(body: { "errors" => [{ "status" => 401, "title" => "token has expired." }] }) if orcid_token_expired

# Don't go to orcid if we've got a claimed_at date but marked as still to create with no put_code
# return OpenStruct.new(body: { "skip" => true, "reason" => "Already claimed." }) if to_be_created? && !put_code.present? && claimed_at.present?
Expand All @@ -280,6 +280,15 @@ def create_uuid
write_attribute(:uuid, SecureRandom.uuid) if uuid.blank?
end

def orcid_token
source_id == "orcid_search" ? user.orcid_search_and_link_access_token : user.orcid_auto_update_access_token
end

def orcid_token_expired
expires_at = source_id == "orcid_search" ? user.orcid_search_and_link_expires_at : user.orcid_auto_update_expires_at
(Date.new(1970, 1, 2).beginning_of_day..Date.today.end_of_day) === expires_at
end

def work
sandbox = ENV["SANDBOX"].present? || (ENV["ORCID_URL"] == "https://sandbox.orcid.org")
# Note that if this is ever intended in future to support claiming for non datacite dois
Expand Down
24 changes: 22 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ class User < ApplicationRecord
indexes :role_id, type: :keyword
indexes :role_name, type: :keyword
indexes :orcid_token, type: :keyword
indexes :orcid_auto_update_access_token, type: :keyword
indexes :orcid_auto_update_refresh_token, type: :keyword
indexes :orcid_auto_update_expires_at, type: :date
indexes :orcid_search_and_link_access_token, type: :keyword
indexes :orcid_search_and_link_refresh_token, type: :keyword
indexes :orcid_search_and_link_expires_at, type: :date
indexes :created, type: :date
indexes :updated, type: :date
indexes :orcid_expires_at, type: :date
Expand Down Expand Up @@ -106,6 +112,12 @@ def as_indexed_json(_options = {})
"is_active" => is_active,
"orcid_token" => orcid_token,
"orcid_expires_at" => orcid_expires_at,
"orcid_auto_update_access_token" => orcid_auto_update_access_token,
"orcid_auto_update_refresh_token" => orcid_auto_update_refresh_token,
"orcid_auto_update_expires_at" => orcid_auto_update_expires_at,
"orcid_search_and_link_access_token" => orcid_search_and_link_access_token,
"orcid_search_and_link_refresh_token" => orcid_search_and_link_refresh_token,
"orcid_search_and_link_expires_at" => orcid_search_and_link_expires_at,
"claims_count" => claims_count,
}
end
Expand All @@ -121,6 +133,10 @@ def self.query_aggregations
}
end

def self.from_orcid(uid)
where(uid: uid).first_or_create
end

def self.from_omniauth(auth, options = {})
where(provider: options[:provider], uid: options[:uid] || auth.uid).first_or_create
end
Expand Down Expand Up @@ -303,7 +319,10 @@ def self.get_auth_hash(auth, options = {})
github: options.fetch("github", nil),
github_uid: options.fetch("github_uid", nil),
github_token: options.fetch("github_token", nil),
email: auth.extra.id_info? ? auth.extra.id_info.email : nil }.compact
email: auth.extra.id_info? ? auth.extra.id_info.email : nil,
orcid_token: auth.credentials.token,
orcid_expires_at: User.timestamp(auth.credentials)
}.compact
end

def self.timestamp(credentials)
Expand Down Expand Up @@ -363,7 +382,8 @@ def get_data(options = {})

Array.wrap(works).select do |work|
work.extend Hashie::Extensions::DeepFetch
work.deep_fetch("work-summary", 0, "source", "source-client-id", "path") { nil } == ENV["ORCID_CLIENT_ID"]
source_client_id = work.deep_fetch("work-summary", 0, "source", "source-client-id", "path") { nil }
source_client_id == ENV["ORCID_AUTO_UPDATE_CLIENT_ID"] || source_client_id == ENV["ORCID_SEARCH_AND_LINK_CLIENT_ID"]
end
end

Expand Down
26 changes: 20 additions & 6 deletions app/views/settings/_show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,28 @@
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>ORCID Permissions</dt>
<dt>Link Works to ORCID</dt>
<dd>
<% if @user.orcid_expires_at && Time.zone.now < @user.orcid_expires_at %>
<p>Delete ORCID token to no longer allow DataCite to update your ORCID record.</p>
<%= link_to "<img id=\"orcid-logo\" src=\"#{ENV["CDN_URL"]}/images/orcid.png\" alt=\"ORCID icon\"/>&nbsp;Delete ORCID Token".html_safe, setting_path("me", user: { orcid_token: nil, orcid_expires_at: Time.zone.now }), { method: :put, remote: true, class: 'btn btn-social btn-orcid btn-fill' } %>
<% if @user.orcid_search_and_link_access_token %>
<p>Revoke access for DataCite to link DataCite DOIs to you ORCID record using the "Add to ORCID Record" button in DataCite Commons</p>
<%= link_to "<img id=\"orcid-logo\" src=\"#{ENV["CDN_URL"]}/images/orcid.png\" alt=\"ORCID icon\"/>&nbsp;Click to Disable".html_safe, :orcid_search_and_link_revoke, method: :get, :id => "orcid-search-and-link-disable", class: 'btn btn-social btn-orcid btn-fill' %>
<% else %>
<p>Get ORCID token to allow DataCite to update your ORCID record.</p>
<%= link_to "<img id=\"orcid-logo\" src=\"#{ENV["CDN_URL"]}/images/orcid.png\" alt=\"ORCID icon\"/>&nbsp;Get ORCID Token".html_safe, user_orcid_omniauth_authorize_path(redirect_to_commons: false), method: :post, :id => "sign-in-orcid", class: 'btn btn-social btn-orcid btn-fill' %>
<p>Allow DataCite to link DataCite DOIs to you ORCID record using the "Add to ORCID Record" button in DataCite Commons</p>
<%= link_to "<img id=\"orcid-logo\" src=\"#{ENV["CDN_URL"]}/images/orcid.png\" alt=\"ORCID icon\"/>&nbsp;Click to Enable".html_safe, :orcid_search_and_link_auth, method: :get, :id => "orcid-search-and-link-enable", class: 'btn btn-social btn-orcid btn-fill' %>
<% end %>
</dd>
</dl>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>ORCID Auto-Update</dt>
<dd>
<% if @user.orcid_auto_update_access_token %>
<p>Revoke access DataCite to add Works to your ORCID record automatically when your ORCID is included as a creator in a DataCite DOI metadata</p>
<%= link_to "<img id=\"orcid-logo\" src=\"#{ENV["CDN_URL"]}/images/orcid.png\" alt=\"ORCID icon\"/>&nbsp;Click to Disable".html_safe, :orcid_auto_update_revoke, method: :get, :id => "orcid-auto-update-disable", class: 'btn btn-social btn-orcid btn-fill' %>
<% else %>
<p>Allow DataCite to add Works to your ORCID record automatically when your ORCID is included as a creator in a DataCite DOI metadata</p>
<%= link_to "<img id=\"orcid-logo\" src=\"#{ENV["CDN_URL"]}/images/orcid.png\" alt=\"ORCID icon\"/>&nbsp;Click to Enable".html_safe, :orcid_auto_update_auth, method: :get, :id => "orcid-auto-update-enable", class: 'btn btn-social btn-orcid btn-fill' %>
<% end %>
</dd>
</dl>
Expand Down
1 change: 1 addition & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
ENV["ORCID_CLIENT_SECRET"],
member: ENV["ORCID_MEMBER"],
sandbox: (ENV["ORCID_URL"] == "https://sandbox.orcid.org"),
scope: "/authenticate",
provider_ignores_state: true

config.omniauth :github, ENV["GITHUB_CLIENT_ID"],
Expand Down
Loading