discourse-oauth2-basic/plugin.rb

294 lines
10 KiB
Ruby

# frozen_string_literal: true
# name: discourse-oauth2-basic
# about: Generic OAuth2 Plugin
# version: 0.3
# authors: Robin Ward
# url: https://github.com/discourse/discourse-oauth2-basic
# transpile_js: true
enabled_site_setting :oauth2_enabled
class ::OmniAuth::Strategies::Oauth2Basic < ::OmniAuth::Strategies::OAuth2
option :name, "oauth2_basic"
uid do
if path = SiteSetting.oauth2_callback_user_id_path.split('.')
recurse(access_token, [*path]) if path.present?
end
end
info do
if paths = SiteSetting.oauth2_callback_user_info_paths.split('|')
result = Hash.new
paths.each do |p|
segments = p.split(':')
if segments.length == 2
key = segments.first
path = [*segments.last.split('.')]
result[key] = recurse(access_token, path)
end
end
result
end
end
def callback_url
Discourse.base_url_no_prefix + script_name + callback_path
end
def recurse(obj, keys)
return nil if !obj
k = keys.shift
result = obj.respond_to?(k) ? obj.send(k) : obj[k]
keys.empty? ? result : recurse(result, keys)
end
end
require 'faraday/logging/formatter'
class OAuth2FaradayFormatter < Faraday::Logging::Formatter
def request(env)
warn <<~LOG
OAuth2 Debugging: request #{env.method.upcase} #{env.url.to_s}
Headers: #{env.request_headers}
Body: #{env[:body]}
LOG
end
def response(env)
warn <<~LOG
OAuth2 Debugging: response status #{env.status}
From #{env.method.upcase} #{env.url.to_s}
Headers: #{env.response_headers}
Body: #{env[:body]}
LOG
end
end
# You should use this register if you want to add custom paths to traverse the user details JSON.
# We'll store the value in the user associated account's extra attribute hash using the full path as the key.
DiscoursePluginRegistry.define_filtered_register :oauth2_basic_additional_json_paths
class ::OAuth2BasicAuthenticator < Auth::ManagedAuthenticator
def name
'oauth2_basic'
end
def can_revoke?
SiteSetting.oauth2_allow_association_change
end
def can_connect_existing_user?
SiteSetting.oauth2_allow_association_change
end
def register_middleware(omniauth)
omniauth.provider :oauth2_basic,
name: name,
setup: lambda { |env|
opts = env['omniauth.strategy'].options
opts[:client_id] = SiteSetting.oauth2_client_id
opts[:client_secret] = SiteSetting.oauth2_client_secret
opts[:provider_ignores_state] = SiteSetting.oauth2_disable_csrf
opts[:client_options] = {
authorize_url: SiteSetting.oauth2_authorize_url,
token_url: SiteSetting.oauth2_token_url,
token_method: SiteSetting.oauth2_token_url_method.downcase.to_sym
}
opts[:authorize_options] = SiteSetting.oauth2_authorize_options.split("|").map(&:to_sym)
if SiteSetting.oauth2_authorize_signup_url.present? &&
ActionDispatch::Request.new(env).params["signup"].present?
opts[:client_options][:authorize_url] = SiteSetting.oauth2_authorize_signup_url
end
if SiteSetting.oauth2_send_auth_header? && SiteSetting.oauth2_send_auth_body?
# For maximum compatibility we include both header and body auth by default
# This is a little unusual, and utilising multiple authentication methods
# is technically disallowed by the spec (RFC2749 Section 5.2)
opts[:client_options][:auth_scheme] = :request_body
opts[:token_params] = { headers: { 'Authorization' => basic_auth_header } }
elsif SiteSetting.oauth2_send_auth_header?
opts[:client_options][:auth_scheme] = :basic_auth
else
opts[:client_options][:auth_scheme] = :request_body
end
unless SiteSetting.oauth2_scope.blank?
opts[:scope] = SiteSetting.oauth2_scope
end
if SiteSetting.oauth2_debug_auth && defined? OAuth2FaradayFormatter
opts[:client_options][:connection_build] = lambda { |builder|
builder.response :logger, Rails.logger, { bodies: true, formatter: OAuth2FaradayFormatter }
# Default stack:
builder.request :url_encoded # form-encode POST params
builder.adapter Faraday.default_adapter # make requests with Net::HTTP
}
end
}
end
def basic_auth_header
"Basic " + Base64.strict_encode64("#{SiteSetting.oauth2_client_id}:#{SiteSetting.oauth2_client_secret}")
end
def walk_path(fragment, segments, seg_index = 0)
first_seg = segments[seg_index]
return if first_seg.blank? || fragment.blank?
return nil unless fragment.is_a?(Hash) || fragment.is_a?(Array)
first_seg = segments[seg_index].scan(/([\d+])/).length > 0 ? first_seg.split("[")[0] : first_seg
if fragment.is_a?(Hash)
deref = fragment[first_seg] || fragment[first_seg.to_sym]
else
array_index = 0
if (seg_index > 0)
last_index = segments[seg_index - 1].scan(/([\d+])/).flatten() || [0]
array_index = last_index.length > 0 ? last_index[0].to_i : 0
end
if fragment.any? && fragment.length >= array_index - 1
deref = fragment[array_index][first_seg]
else
deref = nil
end
end
if (deref.blank? || seg_index == segments.size - 1)
deref
else
seg_index += 1
walk_path(deref, segments, seg_index)
end
end
def json_walk(result, user_json, prop, custom_path: nil)
path = custom_path || SiteSetting.public_send("oauth2_json_#{prop}_path")
if path.present?
#this.[].that is the same as this.that, allows for both this[0].that and this.[0].that path styles
path = path.gsub(".[].", ".").gsub(".[", "[")
segments = parse_segments(path)
val = walk_path(user_json, segments)
result[prop] = val if val.present?
end
end
def parse_segments(path)
segments = [+""]
quoted = false
escaped = false
path.split("").each do |char|
next_char_escaped = false
if !escaped && (char == '"')
quoted = !quoted
elsif !escaped && !quoted && (char == '.')
segments.append +""
elsif !escaped && (char == '\\')
next_char_escaped = true
else
segments.last << char
end
escaped = next_char_escaped
end
segments
end
def log(info)
Rails.logger.warn("OAuth2 Debugging: #{info}") if SiteSetting.oauth2_debug_auth
end
def fetch_user_details(token, id)
user_json_url = SiteSetting.oauth2_user_json_url.sub(':token', token.to_s).sub(':id', id.to_s)
user_json_method = SiteSetting.oauth2_user_json_url_method
log("user_json_url: #{user_json_method} #{user_json_url}")
bearer_token = "Bearer #{token}"
connection = Excon.new(
user_json_url,
headers: { 'Authorization' => bearer_token, 'Accept' => 'application/json' }
)
user_json_response = connection.request(method: user_json_method)
log("user_json_response: #{user_json_response.inspect}")
if user_json_response.status == 200
user_json = JSON.parse(user_json_response.body)
log("user_json: #{user_json}")
result = {}
if user_json.present?
json_walk(result, user_json, :user_id)
json_walk(result, user_json, :username)
json_walk(result, user_json, :name)
json_walk(result, user_json, :email)
json_walk(result, user_json, :email_verified)
json_walk(result, user_json, :avatar)
DiscoursePluginRegistry.oauth2_basic_additional_json_paths.each do |detail|
prop = "extra:#{detail}"
json_walk(result, user_json, prop, custom_path: detail)
end
end
result
else
nil
end
end
def primary_email_verified?(auth)
return true if SiteSetting.oauth2_email_verified
verified = auth['info']['email_verified']
verified = true if verified == "true"
verified = false if verified == "false"
verified
end
def always_update_user_email?
SiteSetting.oauth2_overrides_email
end
def after_authenticate(auth, existing_account: nil)
log("after_authenticate response: \n\ncreds: #{auth['credentials'].to_hash}\nuid: #{auth['uid']}\ninfo: #{auth['info'].to_hash}\nextra: #{auth['extra'].to_hash}")
if SiteSetting.oauth2_fetch_user_details?
if fetched_user_details = fetch_user_details(auth['credentials']['token'], auth['uid'])
auth['uid'] = fetched_user_details[:user_id] if fetched_user_details[:user_id]
auth['info']['nickname'] = fetched_user_details[:username] if fetched_user_details[:username]
auth['info']['image'] = fetched_user_details[:avatar] if fetched_user_details[:avatar]
['name', 'email', 'email_verified'].each do |property|
auth['info'][property] = fetched_user_details[property.to_sym] if fetched_user_details[property.to_sym]
end
DiscoursePluginRegistry.oauth2_basic_additional_json_paths.each do |detail|
auth['extra'][detail] = fetched_user_details["extra:#{detail}"]
end
else
result = Auth::Result.new
result.failed = true
result.failed_reason = I18n.t("login.authenticator_error_fetch_user_details")
return result
end
end
super(auth, existing_account: existing_account)
end
def enabled?
SiteSetting.oauth2_enabled
end
end
auth_provider title_setting: "oauth2_button_title",
authenticator: OAuth2BasicAuthenticator.new
load File.expand_path("../lib/validators/oauth2_basic/oauth2_fetch_user_details_validator.rb", __FILE__)