discourse/app/controllers/users/omniauth_callbacks_controller.rb
Angus McLeod df3886d6e5
FEATURE: Experimental support for group membership via google auth (#14835)
This commit introduces a new site setting "google_oauth2_hd_groups". If enabled, group information will be fetched from Google during authentication, and stored in the Discourse database. These 'associated groups' can be connected to a Discourse group via the "Membership" tab of the group preferences UI. 

The majority of the implementation is generic, so we will be able to add support to more authentication methods in the near future.

https://meta.discourse.org/t/managing-group-membership-via-authentication/175950
2021-12-09 12:30:27 +00:00

200 lines
6.5 KiB
Ruby

# -*- encoding : utf-8 -*-
# frozen_string_literal: true
class Users::OmniauthCallbacksController < ApplicationController
skip_before_action :redirect_to_login_if_required
layout 'no_ember'
# need to be able to call this
skip_before_action :check_xhr
# this is the only spot where we allow CSRF, our openid / oauth redirect
# will not have a CSRF token, however the payload is all validated so its safe
skip_before_action :verify_authenticity_token, only: :complete
def confirm_request
self.class.find_authenticator(params[:provider])
render locals: { hide_auth_buttons: true }
end
def complete
auth = request.env["omniauth.auth"]
raise Discourse::NotFound unless request.env["omniauth.auth"]
auth[:session] = session
authenticator = self.class.find_authenticator(params[:provider])
if session.delete(:auth_reconnect) && authenticator.can_connect_existing_user? && current_user
path = persist_auth_token(auth)
return redirect_to path
else
DiscourseEvent.trigger(:before_auth, authenticator, auth, session, cookies, request)
@auth_result = authenticator.after_authenticate(auth)
@auth_result.user = nil if @auth_result&.user&.staged # Treat staged users the same as unregistered users
DiscourseEvent.trigger(:after_auth, authenticator, @auth_result, session, cookies, request)
end
preferred_origin = request.env['omniauth.origin']
if session[:destination_url].present?
preferred_origin = session[:destination_url]
session.delete(:destination_url)
elsif SiteSetting.enable_discourse_connect_provider && payload = cookies.delete(:sso_payload)
preferred_origin = session_sso_provider_url + "?" + payload
elsif cookies[:destination_url].present?
preferred_origin = cookies[:destination_url]
cookies.delete(:destination_url)
end
if preferred_origin.present?
parsed = begin
URI.parse(preferred_origin)
rescue URI::Error
end
if valid_origin?(parsed)
@origin = +"#{parsed.path}"
@origin << "?#{parsed.query}" if parsed.query
end
end
if @origin.blank?
@origin = Discourse.base_path("/")
end
@auth_result.destination_url = @origin
@auth_result.authenticator_name = authenticator.name
return render_auth_result_failure if @auth_result.failed?
complete_response_data
return render_auth_result_failure if @auth_result.failed?
client_hash = @auth_result.to_client_hash
if authenticator.can_connect_existing_user? &&
(SiteSetting.enable_local_logins || Discourse.enabled_authenticators.count > 1)
# There is more than one login method, and users are allowed to manage associations themselves
client_hash[:associate_url] = persist_auth_token(auth)
end
cookies['_bypass_cache'] = true
cookies[:authentication_data] = {
value: client_hash.to_json,
path: Discourse.base_path("/")
}
redirect_to @origin
end
def valid_origin?(uri)
return false if uri.nil?
return false if uri.host.present? && uri.host != Discourse.current_hostname
return false if uri.path.start_with?("#{Discourse.base_path}/auth/")
return false if uri.path.start_with?("#{Discourse.base_path}/login")
true
end
def failure
error_key = params[:message].to_s.gsub(/[^\w-]/, "")
error_key = "generic" if error_key.blank?
flash[:error] = I18n.t(
"login.omniauth_error.#{error_key}",
default: I18n.t("login.omniauth_error.generic")
).html_safe
render 'failure'
end
def self.find_authenticator(name)
Discourse.enabled_authenticators.each do |authenticator|
return authenticator if authenticator.name == name
end
raise Discourse::InvalidAccess.new(I18n.t('authenticator_not_found'))
end
protected
def render_auth_result_failure
flash[:error] = @auth_result.failed_reason.html_safe
render 'failure'
end
def complete_response_data
if @auth_result.user
user_found(@auth_result.user)
elsif invite_required?
@auth_result.requires_invite = true
else
session[:authentication] = @auth_result.session_data
end
end
def invite_required?
if SiteSetting.invite_only?
path = Discourse.route_for(@origin)
return true unless path
return true if path[:controller] != "invites" && path[:action] != "show"
!Invite.exists?(invite_key: path[:id])
end
end
def user_found(user)
if user.has_any_second_factor_methods_enabled?
@auth_result.omniauth_disallow_totp = true
@auth_result.email = user.email
return
end
# automatically activate any account if a provider marked the email valid
if @auth_result.email_valid && @auth_result.email == user.email
if !user.active || !user.email_confirmed?
user.update!(password: SecureRandom.hex)
# Ensure there is an active email token
if !EmailToken.where(email: user.email, confirmed: true).exists? && !user.email_tokens.active.where(email: user.email).exists?
user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
end
user.activate
end
user.update!(registration_ip_address: request.remote_ip) if user.registration_ip_address.blank?
end
if ScreenedIpAddress.should_block?(request.remote_ip)
@auth_result.not_allowed_from_ip_address = true
elsif ScreenedIpAddress.block_admin_login?(user, request.remote_ip)
@auth_result.admin_not_allowed_from_ip_address = true
elsif Guardian.new(user).can_access_forum? && user.active # log on any account that is active with forum access
begin
user.save! if @auth_result.apply_user_attributes!
@auth_result.apply_associated_attributes!
rescue ActiveRecord::RecordInvalid => e
@auth_result.failed = true
@auth_result.failed_reason = e.record.errors.full_messages.join(", ")
return
end
log_on_user(user)
Invite.invalidate_for_email(user.email) # invite link can't be used to log in anymore
session[:authentication] = nil # don't carry around old auth info, perhaps move elsewhere
@auth_result.authenticated = true
else
if SiteSetting.must_approve_users? && !user.approved?
@auth_result.awaiting_approval = true
else
@auth_result.awaiting_activation = true
end
end
end
def persist_auth_token(auth)
secret = SecureRandom.hex
secure_session.set "#{Users::AssociateAccountsController.key(secret)}", auth.to_json, expires: 10.minutes
"#{Discourse.base_path}/associate/#{secret}"
end
end