require_dependency 'has_errors' class Auth::GithubAuthenticator < Auth::Authenticator def name "github" end def enabled? SiteSetting.enable_github_logins end def description_for_user(user) info = GithubUserInfo.find_by(user_id: user.id) info&.screen_name || "" end def can_revoke? true end def revoke(user, skip_remote: false) info = GithubUserInfo.find_by(user_id: user.id) raise Discourse::NotFound if info.nil? info.destroy! true end class GithubEmailChecker include ::HasErrors def initialize(validator, email) @validator = validator @email = Email.downcase(email) end def valid?() @validator.validate_each(self, :email, @email) return errors.blank? end end def can_connect_existing_user? true end def after_authenticate(auth_token, existing_account: nil) result = Auth::Result.new data = auth_token[:info] result.username = screen_name = data[:nickname] result.name = data[:name] github_user_id = auth_token[:uid] result.extra_data = { github_user_id: github_user_id, github_screen_name: screen_name, } user_info = GithubUserInfo.find_by(github_user_id: github_user_id) if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) user_info.destroy! if user_info user_info = GithubUserInfo.create( user_id: existing_account.id, screen_name: screen_name, github_user_id: github_user_id ) end if user_info # If there's existing user info with the given GitHub ID, that's all we # need to know. user = user_info.user result.email = data[:email] result.email_valid = data[:email].present? else # Potentially use *any* of the emails from GitHub to find a match or # register a new user, with preference given to the primary email. all_emails = Array.new(auth_token[:extra][:all_emails]) primary = all_emails.detect { |email| email[:primary] && email[:verified] } all_emails.unshift(primary) if primary.present? # Only consider verified emails to match an existing user. We don't want # someone to be able to create a GitHub account with an unverified email # in order to access someone else's Discourse account! all_emails.each do |candidate| if !!candidate[:verified] && (user = User.find_by_email(candidate[:email])) result.email = candidate[:email] result.email_valid = !!candidate[:verified] GithubUserInfo .where('user_id = ? OR github_user_id = ?', user.id, github_user_id) .destroy_all GithubUserInfo.create!( user_id: user.id, screen_name: screen_name, github_user_id: github_user_id ) break end end # If we *still* don't have a user, check to see if there's an email that # passes validation (this includes whitelist/blacklist filtering if any is # configured). When no whitelist/blacklist is in play, this will simply # choose the primary email since it's at the front of the list. if !user validator = EmailValidator.new(attributes: :email) found_email = false all_emails.each do |candidate| checker = GithubEmailChecker.new(validator, candidate[:email]) if checker.valid? result.email = candidate[:email] result.email_valid = !!candidate[:verified] found_email = true break end end if !found_email result.failed = true escaped = Rack::Utils.escape_html(screen_name) result.failed_reason = I18n.t("login.authenticator_error_no_valid_email", account: escaped) end end end retrieve_avatar(user, data) result.user = user result end def after_create_account(user, auth) data = auth[:extra_data] GithubUserInfo.create( user_id: user.id, screen_name: data[:github_screen_name], github_user_id: data[:github_user_id] ) retrieve_avatar(user, data) end def register_middleware(omniauth) omniauth.provider :github, setup: lambda { |env| strategy = env["omniauth.strategy"] strategy.options[:client_id] = SiteSetting.github_client_id strategy.options[:client_secret] = SiteSetting.github_client_secret }, scope: "user:email" end private def retrieve_avatar(user, data) return unless data[:image].present? && user && user.user_avatar&.custom_upload_id.blank? Jobs.enqueue(:download_avatar_from_url, url: data[:image], user_id: user.id, override_gravatar: false) end end