require_dependency 'single_sign_on' class DiscourseSingleSignOn < SingleSignOn def self.sso_url SiteSetting.sso_url end def self.sso_secret SiteSetting.sso_secret end def self.generate_url(return_path="/") sso = new sso.nonce = SecureRandom.hex sso.register_nonce(return_path) sso.to_url end def register_nonce(return_path) if nonce $redis.setex(nonce_key, NONCE_EXPIRY_TIME, return_path) end end def nonce_valid? nonce && $redis.get(nonce_key).present? end def return_path $redis.get(nonce_key) || "/" end def expire_nonce! if nonce $redis.del nonce_key end end def nonce_key "SSO_NONCE_#{nonce}" end def lookup_or_create_user sso_record = SingleSignOnRecord.find_by(external_id: external_id) if sso_record && user = sso_record.user sso_record.last_payload = unsigned_payload else user = match_email_or_create_user sso_record = user.single_sign_on_record end # if the user isn't new or it's attached to the SSO record we might be overriding username or email unless user.new_record? change_external_attributes_and_override(sso_record, user) end if sso_record && (user = sso_record.user) && !user.active user.active = true user.save! user.enqueue_welcome_message('welcome_user') end custom_fields.each do |k,v| user.custom_fields[k] = v end # optionally save the user and sso_record if they have changed user.save! sso_record.save! sso_record && sso_record.user end private def match_email_or_create_user user = User.find_by_email(email) try_name = name.blank? ? nil : name try_username = username.blank? ? nil : username user_params = { email: email, name: User.suggest_name(try_name || try_username || email), username: UserNameSuggester.suggest(try_username || try_name || email), } if user || user = User.create!(user_params) if sso_record = user.single_sign_on_record sso_record.last_payload = unsigned_payload sso_record.external_id = external_id else user.create_single_sign_on_record(last_payload: unsigned_payload, external_id: external_id, external_username: username, external_email: email, external_name: name) end end user end def change_external_attributes_and_override(sso_record, user) if SiteSetting.sso_overrides_email && email != sso_record.external_email # set the user's email to whatever came in the payload user.email = email end if SiteSetting.sso_overrides_username && username != sso_record.external_username && user.username != username # we have an external username change, and the user's current username doesn't match # run it through the UserNameSuggester to override it user.username = UserNameSuggester.suggest(username || name || email) end if SiteSetting.sso_overrides_name && name != sso_record.external_name && user.name != name # we have an external name change, and the user's current name doesn't match # run it through the name suggester to override it user.name = User.suggest_name(name || username || email) end if SiteSetting.sso_overrides_avatar && ( avatar_force_update == "true" || avatar_force_update.to_i != 0 || sso_record.external_avatar_url != avatar_url) begin tempfile = FileHelper.download(avatar_url, 1.megabyte, "sso-avatar", true) ext = FastImage.type(tempfile).to_s tempfile.rewind upload = Upload.create_for(user.id, tempfile, "external-avatar." + ext, File.size(tempfile.path), { origin: avatar_url }) user.uploaded_avatar_id = upload.id if !user.user_avatar.contains_upload?(upload.id) user.user_avatar.custom_upload_id = upload.id end rescue SocketError # skip saving, we are not connected to the net Rails.logger.warn "Failed to download external avatar: #{avatar_url}, socket error - user id #{ user.id }" ensure tempfile.close! if tempfile && tempfile.respond_to?(:close!) end end # change external attributes for sso record sso_record.external_username = username sso_record.external_email = email sso_record.external_name = name sso_record.external_avatar_url = avatar_url end end