diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index 6f505992..f5e2a95a 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -4,7 +4,7 @@ module Api class ActiveStorageBlobsProxyController < ApiBaseController include ActiveStorage::Streaming - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check before_action :set_cors_headers diff --git a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb index 77ad2c6a..20675d41 100644 --- a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb @@ -4,7 +4,7 @@ module Api class ActiveStorageBlobsProxyLegacyController < ApiBaseController include ActiveStorage::Streaming - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check before_action :set_cors_headers diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb index b681aa67..05e56eb3 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -12,7 +12,7 @@ module Api wrap_parameters false - before_action :authenticate_user! + before_action :authenticate_via_token! check_authorization rescue_from Params::BaseValidator::InvalidParameterError do |e| @@ -81,7 +81,7 @@ module Api result end - def authenticate_user! + def authenticate_via_token! render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user end diff --git a/app/controllers/api/external_auth_controller.rb b/app/controllers/api/external_auth_controller.rb new file mode 100644 index 00000000..497a4a8f --- /dev/null +++ b/app/controllers/api/external_auth_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Api + class ExternalAuthController < Api::ApiBaseController + skip_before_action :authenticate_via_token! + skip_authorization_check + + def user_token + account = Account.find_or_create_by_external_id( + params[:account][:external_id]&.to_i, + name: params[:account][:name], + locale: params[:account][:locale] || 'en-US', + timezone: params[:account][:timezone] || 'UTC' + ) + + user = User.find_or_create_by_external_id( + account, + params[:user][:external_id]&.to_i, + email: params[:user][:email], + first_name: params[:user][:first_name], + last_name: params[:user][:last_name], + role: 'admin' + ) + + render json: { access_token: user.access_token.token } + rescue StandardError => e + Rails.logger.error("External auth error: #{e.message}") + Rollbar.error(e) if defined?(Rollbar) + render json: { error: 'Internal server error' }, status: :internal_server_error + end + end +end diff --git a/app/controllers/api/submitter_email_clicks_controller.rb b/app/controllers/api/submitter_email_clicks_controller.rb index cef26542..aaf97546 100644 --- a/app/controllers/api/submitter_email_clicks_controller.rb +++ b/app/controllers/api/submitter_email_clicks_controller.rb @@ -2,7 +2,7 @@ module Api class SubmitterEmailClicksController < ApiBaseController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def create diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb index e8b52095..d2931139 100644 --- a/app/controllers/api/submitter_form_views_controller.rb +++ b/app/controllers/api/submitter_form_views_controller.rb @@ -2,7 +2,7 @@ module Api class SubmitterFormViewsController < ApiBaseController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def create diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 92bf197c..421313aa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,14 +6,12 @@ class ApplicationController < ActionController::Base include ActiveStorage::SetCurrent include Pagy::Backend - before_action :ensure_demo_user_signed_in - check_authorization unless: :devise_controller? around_action :with_locale # before_action :sign_in_for_demo, if: -> { Docuseal.demo? } - before_action :maybe_redirect_to_setup, unless: :signed_in? - before_action :authenticate_user!, unless: :devise_controller? + before_action :maybe_authenticate_via_token + before_action :authenticate_via_token!, unless: :devise_controller? helper_method :button_title, :current_account, @@ -102,34 +100,42 @@ class ApplicationController < ActionController::Base current_user&.account end - def maybe_redirect_to_setup - # Skip setup redirect for iframe embedding - create demo user instead - return if ensure_demo_user_signed_in + def maybe_authenticate_via_token + return if signed_in? - redirect_to setup_index_path unless User.exists? - end + # Check for token in params, session, or X-Auth-Token header + token = params[:auth_token] || session[:auth_token] || request.headers['X-Auth-Token'] + return if token.blank? + + # Try to find user by token and sign them in + sha256 = Digest::SHA256.hexdigest(token) + user = User.joins(:access_token).active.find_by(access_token: { sha256: sha256 }) - def ensure_demo_user_signed_in - return true if signed_in? + return unless user - user = find_or_create_demo_user sign_in(user) - true - end - - def find_or_create_demo_user - User.find_by(email: 'demo@docuseal.local') || begin - account = Account.create!(name: 'Demo Account', locale: 'en', timezone: 'UTC') - User.create!( - email: 'demo@docuseal.local', - password: 'password123', - password_confirmation: 'password123', - first_name: 'Demo', - last_name: 'User', - account: account, - role: 'admin' - ) + session[:auth_token] = token + end + + # Enhanced authentication that tries token auth and fails with error if no user found + # Use this when you need to enforce authentication with better token handling + def authenticate_via_token! + return if signed_in? + + token = params[:auth_token] || session[:auth_token] || request.headers['X-Auth-Token'] + + if token.present? + sha256 = Digest::SHA256.hexdigest(token) + user = User.joins(:access_token).active.find_by(access_token: { sha256: sha256 }) + + if user + sign_in(user) + session[:auth_token] = token + return + end end + + render json: { error: 'Authentication required. Please provide a valid auth_token.' }, status: :unauthorized end def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil, diff --git a/app/controllers/concerns/iframe_authentication.rb b/app/controllers/concerns/iframe_authentication.rb new file mode 100644 index 00000000..0683b361 --- /dev/null +++ b/app/controllers/concerns/iframe_authentication.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module IframeAuthentication + extend ActiveSupport::Concern + + private + + # Custom authentication for iframe context + # AJAX requests from Vue components don't include the auth token that's in the iframe URL, + # so we extract it from the HTTP referer header as a fallback + def authenticate_from_referer + return if signed_in? + + token = params[:auth_token] || session[:auth_token] || request.headers['X-Auth-Token'] + + # If no token found, extract from referer URL (iframe page has the token) + if token.blank? && request.referer.present? + referer_uri = URI.parse(request.referer) + referer_params = CGI.parse(referer_uri.query || '') + token = referer_params['auth_token']&.first + end + + if token.present? + sha256 = Digest::SHA256.hexdigest(token) + user = User.joins(:access_token).active.find_by(access_token: { sha256: sha256 }) + + return unless user + + sign_in(user) + session[:auth_token] = token + return + end + + Rails.logger.error "#{self.class.name}: Authentication failed" + render json: { error: 'Authentication required' }, status: :unauthorized + end +end diff --git a/app/controllers/console_redirect_controller.rb b/app/controllers/console_redirect_controller.rb index dd80e9fe..ca815f60 100644 --- a/app/controllers/console_redirect_controller.rb +++ b/app/controllers/console_redirect_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ConsoleRedirectController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def index diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 23df8322..acf25d1d 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class DashboardController < ApplicationController - skip_before_action :authenticate_user!, only: %i[index] + skip_before_action :authenticate_via_token!, only: %i[index] before_action :maybe_redirect_product_url before_action :maybe_render_landing diff --git a/app/controllers/enquiries_controller.rb b/app/controllers/enquiries_controller.rb index 829b578c..98c09f92 100644 --- a/app/controllers/enquiries_controller.rb +++ b/app/controllers/enquiries_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class EnquiriesController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def create diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb index 3c5403b9..3ec5992a 100644 --- a/app/controllers/export_controller.rb +++ b/app/controllers/export_controller.rb @@ -4,7 +4,6 @@ require 'faraday' class ExportController < ApplicationController skip_authorization_check - skip_before_action :maybe_redirect_to_setup skip_before_action :verify_authenticity_token # Send template to third party. diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index 45852360..d916440f 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -3,7 +3,7 @@ class SendSubmissionEmailController < ApplicationController layout 'form' - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_before_action :verify_authenticity_token skip_authorization_check diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb index 778255bc..ba2911f8 100644 --- a/app/controllers/setup_controller.rb +++ b/app/controllers/setup_controller.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class SetupController < ApplicationController - skip_before_action :maybe_redirect_to_setup - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check before_action :redirect_to_root_if_signed, if: :signed_in? diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index fb2a9786..fcc87e34 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -3,7 +3,7 @@ class StartFormController < ApplicationController layout 'form' - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check around_action :with_browser_locale, only: %i[show completed] diff --git a/app/controllers/submissions_debug_controller.rb b/app/controllers/submissions_debug_controller.rb index 4a6e8b9d..c5482020 100644 --- a/app/controllers/submissions_debug_controller.rb +++ b/app/controllers/submissions_debug_controller.rb @@ -3,7 +3,7 @@ class SubmissionsDebugController < ApplicationController layout 'plain' - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def index diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 62836650..f0f3778c 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SubmissionsDownloadController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check TTL = 40.minutes diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index d8a36f0b..c56e8db9 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -2,7 +2,7 @@ class SubmissionsPreviewController < ApplicationController around_action :with_browser_locale - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check prepend_before_action :maybe_redirect_com, only: %i[show completed] diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 24092dcc..e2be5cba 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -4,7 +4,7 @@ class SubmitFormController < ApplicationController layout 'form' around_action :with_browser_locale, only: %i[show completed success] - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check skip_before_action :verify_authenticity_token, only: :update diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 15572da1..99ea9870 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SubmitFormDeclineController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def create diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index d357019c..67815e5e 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SubmitFormDownloadController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check FILES_TTL = 5.minutes diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index f8352ade..7c327b71 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -4,7 +4,7 @@ class SubmitFormDrawSignatureController < ApplicationController layout false around_action :with_browser_locale, only: %i[show] - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def show diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index 1d42779c..1d60e717 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SubmitFormInviteController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def create diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb index 2c4a2ad3..7c3f651d 100644 --- a/app/controllers/submit_form_values_controller.rb +++ b/app/controllers/submit_form_values_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SubmitFormValuesController < ApplicationController - skip_before_action :authenticate_user! + skip_before_action :authenticate_via_token! skip_authorization_check def index diff --git a/app/controllers/submitters_request_changes_controller.rb b/app/controllers/submitters_request_changes_controller.rb index b18d8ed0..dff9f862 100644 --- a/app/controllers/submitters_request_changes_controller.rb +++ b/app/controllers/submitters_request_changes_controller.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class SubmittersRequestChangesController < ApplicationController - before_action :load_submitter + include IframeAuthentication + skip_before_action :verify_authenticity_token, only: :request_changes + skip_before_action :authenticate_via_token!, only: :request_changes + before_action :authenticate_from_referer, only: :request_changes + before_action :load_submitter def request_changes if request.get? || request.head? @@ -48,9 +52,9 @@ class SubmittersRequestChangesController < ApplicationController end def can_request_changes? - # Only the user who created the submission can request changes + # Only the template author (manager) can request changes from submitters # Only for completed submissions that haven't been declined - current_user == @submitter.submission.created_by_user && + current_user == @submitter.submission.template.author && @submitter.completed_at? && !@submitter.declined_at? && !@submitter.changes_requested_at? diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index 7144f5e0..7aaa039c 100644 --- a/app/controllers/template_documents_controller.rb +++ b/app/controllers/template_documents_controller.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true class TemplateDocumentsController < ApplicationController + include IframeAuthentication + skip_before_action :verify_authenticity_token - load_and_authorize_resource :template + skip_before_action :authenticate_via_token! + + before_action :authenticate_from_referer + load_and_authorize_resource :template, id_param: :template_id def create if params[:blobs].blank? && params[:files].blank? diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 12258115..2e2e5919 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -2,12 +2,13 @@ class TemplatesController < ApplicationController include PrefillFieldsHelper + include IframeAuthentication - skip_before_action :maybe_redirect_to_setup skip_before_action :verify_authenticity_token + skip_before_action :authenticate_via_token!, only: [:update] + before_action :authenticate_from_referer, only: [:update] load_and_authorize_resource :template - before_action :load_base_template, only: %i[new create] def show @@ -67,6 +68,7 @@ class TemplatesController < ApplicationController name: params.dig(:template, :name), folder_name: params[:folder_name]) else + @template = Template.new(template_params) if @template.nil? @template.author = current_user @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) end diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 6cb553c1..8690bed6 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class TemplatesDashboardController < ApplicationController - before_action :ensure_demo_user_signed_in - skip_before_action :authenticate_user! - skip_before_action :maybe_redirect_to_setup - load_and_authorize_resource :template_folder, parent: false load_and_authorize_resource :template, parent: false diff --git a/app/models/account.rb b/app/models/account.rb index 6265734d..4a1b7701 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -4,18 +4,20 @@ # # Table name: accounts # -# id :bigint not null, primary key -# archived_at :datetime -# locale :string not null -# name :string not null -# timezone :string not null -# uuid :string not null -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# archived_at :datetime +# locale :string not null +# name :string not null +# timezone :string not null +# uuid :string not null +# created_at :datetime not null +# updated_at :datetime not null +# external_account_id :integer # # Indexes # -# index_accounts_on_uuid (uuid) UNIQUE +# index_accounts_on_external_account_id (external_account_id) UNIQUE +# index_accounts_on_uuid (uuid) UNIQUE # class Account < ApplicationRecord attribute :uuid, :string, default: -> { SecureRandom.uuid } @@ -53,8 +55,15 @@ class Account < ApplicationRecord attribute :timezone, :string, default: 'UTC' attribute :locale, :string, default: 'en-US' + validates :external_account_id, uniqueness: true, allow_nil: true + scope :active, -> { where(archived_at: nil) } + def self.find_or_create_by_external_id(external_id, attributes = {}) + find_by(external_account_id: external_id) || + create!(attributes.merge(external_account_id: external_id)) + end + def testing? linked_account_account&.testing? end diff --git a/app/models/user.rb b/app/models/user.rb index 98e500ee..542924ce 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -29,11 +29,13 @@ # created_at :datetime not null # updated_at :datetime not null # account_id :integer not null +# external_user_id :integer # # Indexes # # index_users_on_account_id (account_id) # index_users_on_email (email) UNIQUE +# index_users_on_external_user_id (external_user_id) UNIQUE # index_users_on_reset_password_token (reset_password_token) UNIQUE # index_users_on_unlock_token (unlock_token) UNIQUE # index_users_on_uuid (uuid) UNIQUE @@ -74,6 +76,17 @@ class User < ApplicationRecord scope :admins, -> { where(role: ADMIN_ROLE) } validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ } + validates :external_user_id, uniqueness: true, allow_nil: true + + def self.find_or_create_by_external_id(account, external_id, attributes = {}) + account.users.find_by(external_user_id: external_id) || + account.users.create!( + attributes.merge( + external_user_id: external_id, + password: SecureRandom.hex(16) + ) + ) + end def access_token super || build_access_token.tap(&:save!) diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 7e878842..122521b6 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -235,7 +235,7 @@ <%= button_to t('resubmit'), submitters_resubmit_path(submitter), method: :put, class: 'btn btn-sm btn-primary w-full', form: { target: '_blank' }, data: { turbo: false } %> <% end %> - <% if signed_in? && submitter && submitter.completed_at? && !submitter.declined_at? && !submitter.changes_requested_at? && current_user == @submission.created_by_user %> + <% if signed_in? && submitter && submitter.completed_at? && !submitter.declined_at? && !submitter.changes_requested_at? && current_user == @submission.template.author %>