mirror of https://github.com/docusealco/docuseal
				
				
				
			
							parent
							
								
									5f04a2d098
								
							
						
					
					
						commit
						f2f1745526
					
				| @ -0,0 +1,91 @@ | ||||
| FROM ruby:3.4.2-alpine AS download | ||||
| 
 | ||||
| WORKDIR /fonts | ||||
| 
 | ||||
| RUN apk --no-cache add fontforge wget && \ | ||||
|     wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \ | ||||
|     wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \ | ||||
|     wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \ | ||||
|     wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \ | ||||
|     wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \ | ||||
|     wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \ | ||||
|     wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \ | ||||
|     mkdir -p /pdfium-linux && \ | ||||
|     tar -xzf pdfium-linux.tgz -C /pdfium-linux | ||||
| 
 | ||||
| RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' | ||||
| 
 | ||||
| FROM ruby:3.4.2-alpine AS app | ||||
| 
 | ||||
| # Development environment settings | ||||
| ENV RAILS_ENV=development | ||||
| ENV NODE_ENV=development | ||||
| ENV BUNDLE_WITHOUT="" | ||||
| ENV LD_PRELOAD=/lib/libgcompat.so.0 | ||||
| ENV OPENSSL_CONF=/app/openssl_legacy.cnf | ||||
| 
 | ||||
| WORKDIR /app | ||||
| 
 | ||||
| # Install system dependencies including nodejs and yarn for development | ||||
| RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && \ | ||||
|     apk add --no-cache \ | ||||
|     sqlite-dev \ | ||||
|     libpq-dev \ | ||||
|     mariadb-dev \ | ||||
|     vips-dev@edge \ | ||||
|     redis \ | ||||
|     libheif@edge \ | ||||
|     vips-heif@edge \ | ||||
|     gcompat \ | ||||
|     ttf-freefont \ | ||||
|     nodejs \ | ||||
|     yarn \ | ||||
|     git \ | ||||
|     build-base && \ | ||||
|     mkdir /fonts && \ | ||||
|     rm /usr/share/fonts/freefont/FreeSans.otf | ||||
| 
 | ||||
| # Copy fonts from download stage | ||||
| COPY --from=download /fonts /fonts | ||||
| COPY --from=download /pdfium-linux /pdfium-linux | ||||
| 
 | ||||
| # Install PDFium library | ||||
| RUN cp /pdfium-linux/lib/libpdfium.so /usr/local/lib/ && \ | ||||
|     chmod +x /usr/local/lib/libpdfium.so | ||||
| 
 | ||||
| # OpenSSL legacy configuration for compatibility | ||||
| RUN echo $'.include = /etc/ssl/openssl.cnf\n\ | ||||
| \n\ | ||||
| [provider_sect]\n\ | ||||
| default = default_sect\n\ | ||||
| legacy = legacy_sect\n\ | ||||
| \n\ | ||||
| [default_sect]\n\ | ||||
| activate = 1\n\ | ||||
| \n\ | ||||
| [legacy_sect]\n\ | ||||
| activate = 1' >> /app/openssl_legacy.cnf | ||||
| 
 | ||||
| # Copy Gemfile and package.json for dependency installation | ||||
| COPY ./Gemfile ./Gemfile.lock ./ | ||||
| COPY ./package.json ./yarn.lock ./ | ||||
| 
 | ||||
| # Install Ruby gems and Node packages | ||||
| RUN bundle install && \ | ||||
|     yarn install --network-timeout 1000000 | ||||
| 
 | ||||
| # Install shakapacker for asset compilation | ||||
| RUN gem install shakapacker | ||||
| 
 | ||||
| # Create necessary directories | ||||
| RUN mkdir -p log tmp/pids tmp/cache tmp/sockets public/assets | ||||
| 
 | ||||
| # Set up the application | ||||
| COPY ./bin ./bin | ||||
| RUN chmod +x ./bin/* | ||||
| 
 | ||||
| # Expose port 3000 | ||||
| EXPOSE 3000 | ||||
| 
 | ||||
| # Default command for development | ||||
| CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] | ||||
| @ -0,0 +1,221 @@ | ||||
| # DocuSeal Omniauth Implementation Guide | ||||
| 
 | ||||
| This document describes the complete implementation of omniauth/SSO functionality in DocuSeal, restoring the features that were previously gated behind paywall checks. | ||||
| 
 | ||||
| ## Overview | ||||
| 
 | ||||
| We have successfully implemented comprehensive omniauth support with the following providers: | ||||
| - **SAML 2.0** - For enterprise SSO integration | ||||
| - **Google OAuth2** - For Google account authentication   | ||||
| - **Microsoft Graph** - For Microsoft/Office 365 authentication | ||||
| 
 | ||||
| ## What Was Implemented | ||||
| 
 | ||||
| ### 1. Database Schema Changes | ||||
| - Added `provider` and `uid` columns to the `users` table | ||||
| - Created migration: `db/migrate/20250719104801_add_omniauth_to_users.rb` | ||||
| - Added unique index on `[provider, uid]` combination | ||||
| 
 | ||||
| ### 2. User Model Updates | ||||
| - Added `:omniauthable` to Devise modules | ||||
| - Configured omniauth providers: `%i[saml google_oauth2 microsoft_graph]` | ||||
| - Implemented `User.from_omniauth(auth)` class method for handling authentication callbacks | ||||
| - Added support for both single-tenant and multi-tenant account creation | ||||
| 
 | ||||
| ### 3. Devise Configuration | ||||
| - Updated `config/initializers/devise.rb` with omniauth provider configurations | ||||
| - Added environment variable-based configuration for SAML | ||||
| - Configured Google OAuth2 and Microsoft Graph providers | ||||
| - Added proper attribute mapping for SAML assertions | ||||
| 
 | ||||
| ### 4. Controllers | ||||
| - Created `app/controllers/users/omniauth_callbacks_controller.rb` | ||||
| - Handles callbacks for all three providers (SAML, Google, Microsoft) | ||||
| - Includes proper error handling and session management | ||||
| - Updated `app/controllers/sso_settings_controller.rb` with configuration management | ||||
| 
 | ||||
| ### 5. Views and UI | ||||
| - Replaced SSO paywall placeholder with functional SAML configuration form | ||||
| - Updated login form to include all three authentication providers | ||||
| - Added SSO settings interface for SAML configuration | ||||
| - Removed `Docuseal.multitenant?` checks that were gating SSO features | ||||
| 
 | ||||
| ### 6. Routes | ||||
| - Updated routes to include omniauth callbacks | ||||
| - Added update action to SSO settings controller | ||||
| - Enabled SSO settings for all users (removed paywall restrictions) | ||||
| 
 | ||||
| ### 7. Gemfile Dependencies | ||||
| Added the following gems: | ||||
| ```ruby | ||||
| gem 'omniauth' | ||||
| gem 'omniauth-rails_csrf_protection' | ||||
| gem 'omniauth-saml' | ||||
| gem 'omniauth-google-oauth2' | ||||
| gem 'omniauth-microsoft_graph' | ||||
| ``` | ||||
| 
 | ||||
| ### 8. Development Environment | ||||
| - Created `Dockerfile.dev` for development with mounted codebase | ||||
| - Created `docker-compose.dev.yml` for local development environment | ||||
| - Configured development environment with PostgreSQL and Redis | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| ### SAML Configuration | ||||
| 
 | ||||
| #### Environment Variables | ||||
| Set these environment variables for SAML configuration: | ||||
| ```bash | ||||
| SAML_IDP_SSO_SERVICE_URL=https://your-idp.com/sso/saml | ||||
| SAML_IDP_CERT_FINGERPRINT=AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD | ||||
| SAML_SP_ENTITY_ID=docuseal | ||||
| APP_URL=http://localhost:3000 | ||||
| ``` | ||||
| 
 | ||||
| #### Database Configuration | ||||
| Alternatively, configure SAML through the web interface at `/settings/sso` which stores encrypted configuration in the database. | ||||
| 
 | ||||
| #### Service Provider URLs | ||||
| Provide these URLs to your Identity Provider: | ||||
| - **Assertion Consumer Service URL**: `http://localhost:3000/users/auth/saml/callback` | ||||
| - **SP Metadata URL**: `http://localhost:3000/users/auth/saml/metadata` | ||||
| - **SP Entity ID**: `docuseal` (or your custom value) | ||||
| 
 | ||||
| ### Google OAuth2 Configuration | ||||
| 
 | ||||
| Add to Rails credentials or environment variables: | ||||
| ```bash | ||||
| GOOGLE_CLIENT_ID=your-google-client-id | ||||
| GOOGLE_CLIENT_SECRET=your-google-client-secret | ||||
| ``` | ||||
| 
 | ||||
| ### Microsoft Graph Configuration | ||||
| 
 | ||||
| Add to Rails credentials or environment variables: | ||||
| ```bash | ||||
| MICROSOFT_CLIENT_ID=your-microsoft-client-id | ||||
| MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret | ||||
| ``` | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| ### For Users | ||||
| 1. Navigate to the login page | ||||
| 2. Choose from available authentication methods: | ||||
|    - Sign in with Google | ||||
|    - Sign in with Microsoft | ||||
|    - Sign in with SAML SSO (if configured) | ||||
| 3. Complete authentication with your chosen provider | ||||
| 4. You'll be automatically signed in or prompted to complete registration | ||||
| 
 | ||||
| ### For Administrators | ||||
| 1. Go to Settings → SSO | ||||
| 2. Configure SAML settings with your Identity Provider details | ||||
| 3. Test the configuration using the "Test SAML Login" button | ||||
| 4. Users can now authenticate using the configured SSO provider | ||||
| 
 | ||||
| ## Development Setup | ||||
| 
 | ||||
| ### Using Docker (Recommended) | ||||
| ```bash | ||||
| # Build the development environment | ||||
| docker-compose -f docker-compose.dev.yml build | ||||
| 
 | ||||
| # Start the development environment | ||||
| docker-compose -f docker-compose.dev.yml up | ||||
| 
 | ||||
| # Access the application at http://localhost:3000 | ||||
| ``` | ||||
| 
 | ||||
| ### Local Development | ||||
| ```bash | ||||
| # Install dependencies | ||||
| bundle install | ||||
| yarn install | ||||
| 
 | ||||
| # Run database migrations | ||||
| rails db:create db:migrate | ||||
| 
 | ||||
| # Start the development server | ||||
| rails server | ||||
| ``` | ||||
| 
 | ||||
| ## Testing | ||||
| 
 | ||||
| ### SAML Testing | ||||
| 1. Set up environment variables for SAML configuration | ||||
| 2. Navigate to `/settings/sso` to configure SAML | ||||
| 3. Use the "Test SAML Login" button to verify configuration | ||||
| 4. Check logs for any authentication errors | ||||
| 
 | ||||
| ### OAuth Testing | ||||
| 1. Configure Google/Microsoft credentials | ||||
| 2. Navigate to login page | ||||
| 3. Click "Sign in with Google" or "Sign in with Microsoft" | ||||
| 4. Complete OAuth flow and verify user creation/authentication | ||||
| 
 | ||||
| ## Security Considerations | ||||
| 
 | ||||
| 1. **CSRF Protection**: Implemented via `omniauth-rails_csrf_protection` gem | ||||
| 2. **Secure Credentials**: Store sensitive configuration in Rails credentials or environment variables | ||||
| 3. **Certificate Validation**: SAML certificate fingerprints are validated | ||||
| 4. **Session Management**: Proper session cleanup and management implemented | ||||
| 
 | ||||
| ## Troubleshooting | ||||
| 
 | ||||
| ### Common Issues | ||||
| 
 | ||||
| 1. **"NameError: uninitialized constant EncryptedConfig"** | ||||
|    - This was resolved by moving SAML configuration to environment variables | ||||
|    - Ensure proper initialization order in Devise configuration | ||||
| 
 | ||||
| 2. **"Invalid credentials" errors** | ||||
|    - Verify OAuth client IDs and secrets are correct | ||||
|    - Check redirect URIs match exactly | ||||
| 
 | ||||
| 3. **SAML authentication failures** | ||||
|    - Verify IdP certificate fingerprint is correct | ||||
|    - Check that assertion consumer service URL matches | ||||
|    - Ensure name identifier format matches IdP configuration | ||||
| 
 | ||||
| ### Logs | ||||
| Check application logs for detailed error messages: | ||||
| ```bash | ||||
| docker-compose -f docker-compose.dev.yml logs app | ||||
| ``` | ||||
| 
 | ||||
| ## Files Modified/Created | ||||
| 
 | ||||
| ### New Files | ||||
| - `db/migrate/20250719104801_add_omniauth_to_users.rb` | ||||
| - `app/controllers/users/omniauth_callbacks_controller.rb` | ||||
| - `app/views/sso_settings/_saml_form.html.erb` | ||||
| - `Dockerfile.dev` | ||||
| - `docker-compose.dev.yml` | ||||
| - `OMNIAUTH_IMPLEMENTATION.md` | ||||
| 
 | ||||
| ### Modified Files | ||||
| - `Gemfile` - Added omniauth gems | ||||
| - `app/models/user.rb` - Added omniauthable and from_omniauth method | ||||
| - `config/initializers/devise.rb` - Added omniauth provider configurations | ||||
| - `app/controllers/sso_settings_controller.rb` - Added update method | ||||
| - `app/views/sso_settings/index.html.erb` - Replaced placeholder with form | ||||
| - `app/views/devise/sessions/new.html.erb` - Added omniauth provider buttons | ||||
| - `app/views/shared/_settings_nav.html.erb` - Removed paywall check | ||||
| - `config/routes.rb` - Added update route for SSO settings | ||||
| 
 | ||||
| ## Next Steps | ||||
| 
 | ||||
| 1. **Production Deployment**: Configure production environment variables | ||||
| 2. **Additional Providers**: Add more omniauth providers as needed | ||||
| 3. **Advanced SAML**: Implement IdP-initiated SSO and SLO (Single Logout) | ||||
| 4. **User Management**: Add admin interface for managing SSO users | ||||
| 5. **Audit Logging**: Add logging for SSO authentication events | ||||
| 
 | ||||
| ## Support | ||||
| 
 | ||||
| For issues or questions about this implementation, refer to: | ||||
| - [Devise Omniauth Documentation](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview) | ||||
| - [Omniauth SAML Documentation](https://github.com/omniauth/omniauth-saml) | ||||
| - DocuSeal application logs and error messages | ||||
| @ -0,0 +1,141 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController | ||||
|   # See https://github.com/omniauth/omniauth/wiki/FAQ#rails-session-is-clobbered-after-callback-on-developer-strategy | ||||
|   skip_before_action :verify_authenticity_token, only: [:saml, :google_oauth2, :microsoft_graph] | ||||
| 
 | ||||
|   def saml | ||||
|     handle_omniauth_callback('SAML') | ||||
|   end | ||||
| 
 | ||||
|   def google_oauth2 | ||||
|     handle_omniauth_callback('Google') | ||||
|   end | ||||
| 
 | ||||
|   def microsoft_graph | ||||
|     handle_omniauth_callback('Microsoft') | ||||
|   end | ||||
| 
 | ||||
|   def failure | ||||
|     Rails.logger.error "Omniauth failure: #{params[:message]}" | ||||
|     redirect_to new_user_session_path, alert: "Authentication failed: #{params[:message]}" | ||||
|   end | ||||
| 
 | ||||
|   def passthru | ||||
|     # Handle requests to unconfigured or improperly configured SSO providers | ||||
|     provider = params[:provider] || request.env['omniauth.strategy']&.name&.to_s | ||||
|      | ||||
|     case provider | ||||
|     when 'saml' | ||||
|       # Check if SAML is properly configured anywhere in the system | ||||
|       saml_configured = false | ||||
|        | ||||
|       # Check environment variables first | ||||
|       if ENV['SAML_IDP_SSO_SERVICE_URL'].present? && ENV['SAML_IDP_CERT_FINGERPRINT'].present? | ||||
|         saml_configured = true | ||||
|       else | ||||
|         # Check if any account has SAML configured in database | ||||
|         saml_config_record = EncryptedConfig.find_by(key: 'saml_configs') | ||||
|         if saml_config_record&.value.present? | ||||
|           begin | ||||
|             config = JSON.parse(saml_config_record.value) | ||||
|             saml_configured = config['idp_sso_service_url'].present? && config['idp_cert_fingerprint'].present? | ||||
|           rescue JSON::ParserError | ||||
|             # Invalid JSON, treat as not configured | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|        | ||||
|       unless saml_configured | ||||
|         redirect_to new_user_session_path, alert: 'SAML SSO is not configured. Please contact your administrator to set up SAML authentication.' | ||||
|         return | ||||
|       end | ||||
|     when 'google_oauth2' | ||||
|       if Rails.application.credentials.google_client_id.blank? || Rails.application.credentials.google_client_id == 'placeholder_client_id' | ||||
|         redirect_to new_user_session_path, alert: 'Google OAuth is not configured. Please contact your administrator to set up Google authentication.' | ||||
|         return | ||||
|       end | ||||
|     when 'microsoft_graph' | ||||
|       if Rails.application.credentials.microsoft_client_id.blank? || Rails.application.credentials.microsoft_client_id == 'placeholder_client_id' | ||||
|         redirect_to new_user_session_path, alert: 'Microsoft OAuth is not configured. Please contact your administrator to set up Microsoft authentication.' | ||||
|         return | ||||
|       end | ||||
|     end | ||||
|      | ||||
|     # If we get here, redirect to configuration page | ||||
|     provider_name = provider&.humanize || 'SSO' | ||||
|     redirect_to new_user_session_path, alert: "#{provider_name} authentication is not available. Please contact your administrator." | ||||
|   end | ||||
| 
 | ||||
|   def metadata | ||||
|     # Generate basic SAML metadata for the service provider | ||||
|     entity_id = ENV.fetch('SAML_SP_ENTITY_ID', 'docuseal') | ||||
|     app_url = ENV.fetch('APP_URL', 'http://localhost:3000') | ||||
|     callback_url = "#{app_url}/auth/saml/callback" | ||||
|      | ||||
|     # Build comprehensive SP metadata XML | ||||
|     logout_url = "#{app_url}/sign_out" | ||||
|      | ||||
|     metadata_xml = <<~XML | ||||
|       <?xml version="1.0" encoding="UTF-8"?> | ||||
|       <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" | ||||
|                            entityID="#{entity_id}"> | ||||
|         <md:SPSSODescriptor  | ||||
|           AuthnRequestsSigned="false"  | ||||
|           WantAssertionsSigned="true"  | ||||
|           protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> | ||||
|            | ||||
|           <!-- Name ID Format --> | ||||
|           <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat> | ||||
|            | ||||
|           <!-- Assertion Consumer Services --> | ||||
|           <md:AssertionConsumerService | ||||
|             Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" | ||||
|             Location="#{callback_url}" | ||||
|             index="0" | ||||
|             isDefault="true" /> | ||||
|           <md:AssertionConsumerService | ||||
|             Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" | ||||
|             Location="#{callback_url}" | ||||
|             index="1" /> | ||||
|              | ||||
|           <!-- Single Logout Service --> | ||||
|           <md:SingleLogoutService | ||||
|             Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" | ||||
|             Location="#{logout_url}" /> | ||||
|           <md:SingleLogoutService | ||||
|             Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" | ||||
|             Location="#{logout_url}" /> | ||||
|              | ||||
|         </md:SPSSODescriptor> | ||||
|       </md:EntityDescriptor> | ||||
|     XML | ||||
|      | ||||
|     response.headers['Content-Disposition'] = 'attachment; filename="saml-metadata.xml"' | ||||
|     render xml: metadata_xml, content_type: 'application/samlmetadata+xml' | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def handle_omniauth_callback(provider_name) | ||||
|     @user = User.from_omniauth(request.env['omniauth.auth']) | ||||
| 
 | ||||
|     if @user&.persisted? | ||||
|       sign_in_and_redirect @user, event: :authentication | ||||
|       set_flash_message(:notice, :success, kind: provider_name) if is_navigational_format? | ||||
|     else | ||||
|       # Store the omniauth data in session for potential account linking | ||||
|       session['devise.omniauth_data'] = request.env['omniauth.auth'].except(:extra) | ||||
|        | ||||
|       # Redirect to registration with error message | ||||
|       redirect_to new_user_registration_url,  | ||||
|                   alert: "There was an issue with your #{provider_name} account. Please try again or contact support." | ||||
|     end | ||||
|   rescue StandardError => e | ||||
|     Rails.logger.error "Omniauth callback error: #{e.message}" | ||||
|     Rails.logger.error e.backtrace.join("\n") | ||||
|      | ||||
|     redirect_to new_user_session_path,  | ||||
|                 alert: "Authentication failed. Please try again or contact support." | ||||
|   end | ||||
| end | ||||
| @ -0,0 +1,52 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class SamlConfigService | ||||
|   def self.load_config(account = nil) | ||||
|     # Try to load from database first | ||||
|     if account | ||||
|       config_record = EncryptedConfig.find_by(account: account, key: 'saml_configs') | ||||
|       if config_record&.value.present? | ||||
|         return JSON.parse(config_record.value).with_indifferent_access | ||||
|       end | ||||
|     end | ||||
|      | ||||
|     # Fall back to environment variables | ||||
|     { | ||||
|       idp_sso_service_url: ENV['SAML_IDP_SSO_SERVICE_URL'], | ||||
|       idp_cert_fingerprint: ENV['SAML_IDP_CERT_FINGERPRINT'], | ||||
|       sp_entity_id: ENV.fetch('SAML_SP_ENTITY_ID', 'docuseal'), | ||||
|       name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', | ||||
|       email_attribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', | ||||
|       first_name_attribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname', | ||||
|       last_name_attribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname', | ||||
|       name_attribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' | ||||
|     }.with_indifferent_access | ||||
|   rescue => e | ||||
|     Rails.logger.warn "Could not load SAML config: #{e.message}" | ||||
|     {} | ||||
|   end | ||||
|    | ||||
|   def self.configured?(account = nil) | ||||
|     config = load_config(account) | ||||
|     config[:idp_sso_service_url].present? && config[:idp_cert_fingerprint].present? | ||||
|   end | ||||
|    | ||||
|   def self.omniauth_config(account = nil) | ||||
|     config = load_config(account) | ||||
|     return nil unless configured?(account) | ||||
|      | ||||
|     { | ||||
|       assertion_consumer_service_url: "#{ENV.fetch('APP_URL', 'http://localhost:3000')}/auth/saml/callback", | ||||
|       sp_entity_id: config[:sp_entity_id], | ||||
|       idp_sso_service_url: config[:idp_sso_service_url], | ||||
|       idp_cert_fingerprint: config[:idp_cert_fingerprint], | ||||
|       name_identifier_format: config[:name_identifier_format], | ||||
|       attribute_statements: { | ||||
|         email: [config[:email_attribute]], | ||||
|         first_name: [config[:first_name_attribute]], | ||||
|         last_name: [config[:last_name_attribute]], | ||||
|         name: [config[:name_attribute]] | ||||
|       } | ||||
|     } | ||||
|   end | ||||
| end | ||||
| @ -0,0 +1,160 @@ | ||||
| <%= form_with model: @encrypted_config, url: settings_sso_path, method: :patch, local: true, multipart: true, class: "space-y-4" do |f| %> | ||||
|   <div class="space-y-6"> | ||||
|     <div class="alert alert-info"> | ||||
|       <%= svg_icon('info_circle', class: 'w-6 h-6') %> | ||||
|       <div> | ||||
|         <p class="font-bold">SAML SSO Configuration</p> | ||||
|         <p class="text-gray-700"> | ||||
|           Configure SAML 2.0 Single Sign-On for your organization. Users will be able to sign in using your identity provider. | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <% saml_config = @encrypted_config.value.present? ? JSON.parse(@encrypted_config.value) : {} %> | ||||
| 
 | ||||
|     <!-- IdP Metadata Upload Section --> | ||||
|     <div class="card bg-base-100 border border-base-300"> | ||||
|       <div class="card-body"> | ||||
|         <h3 class="card-title text-lg">Quick Setup: Upload IdP Metadata</h3> | ||||
|         <p class="text-sm text-gray-600 mb-4"> | ||||
|           Upload your Identity Provider's metadata XML file to automatically populate the configuration below. | ||||
|         </p> | ||||
|          | ||||
|         <div class="form-control"> | ||||
|           <%= label_tag 'idp_metadata_file', 'IdP Metadata XML File', class: 'label' %> | ||||
|           <%= file_field_tag 'idp_metadata_file',  | ||||
|                              accept: '.xml,application/xml,text/xml',  | ||||
|                              class: 'file-input file-input-bordered w-full' %> | ||||
|           <div class="label"> | ||||
|             <span class="label-text-alt">Select your IdP's metadata.xml file to auto-configure SAML settings</span> | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         <div class="card-actions justify-end mt-4"> | ||||
|           <%= submit_tag 'Parse Metadata', class: 'btn btn-primary btn-sm' %> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="divider">OR configure manually</div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[idp_sso_service_url]', 'Identity Provider SSO URL', class: 'label' %> | ||||
|       <%= text_field_tag 'saml_config[idp_sso_service_url]', saml_config['idp_sso_service_url'],  | ||||
|                          class: 'input input-bordered w-full',  | ||||
|                          placeholder: 'https://your-idp.com/sso/saml', | ||||
|                          required: true %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">The URL where users will be redirected to authenticate</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[idp_cert_fingerprint]', 'Identity Provider Certificate Fingerprint', class: 'label' %> | ||||
|       <%= text_field_tag 'saml_config[idp_cert_fingerprint]', saml_config['idp_cert_fingerprint'],  | ||||
|                          class: 'input input-bordered w-full',  | ||||
|                          placeholder: 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', | ||||
|                          required: true %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">SHA1 fingerprint of your IdP's certificate</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[sp_entity_id]', 'Service Provider Entity ID', class: 'label' %> | ||||
|       <%= text_field_tag 'saml_config[sp_entity_id]', saml_config['sp_entity_id'] || 'docuseal',  | ||||
|                          class: 'input input-bordered w-full',  | ||||
|                          placeholder: 'docuseal', | ||||
|                          required: true %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">Unique identifier for this DocuSeal instance</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[name_identifier_format]', 'Name ID Format', class: 'label' %> | ||||
|       <%= select_tag 'saml_config[name_identifier_format]',  | ||||
|                      options_for_select([ | ||||
|                        ['Email Address', 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'], | ||||
|                        ['Persistent', 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'], | ||||
|                        ['Transient', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'] | ||||
|                      ], saml_config['name_identifier_format'] || 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'), | ||||
|                      class: 'select select-bordered w-full' %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">Format for the user identifier sent by your IdP</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="divider">Attribute Mapping</div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[email_attribute]', 'Email Attribute', class: 'label' %> | ||||
|       <%= text_field_tag 'saml_config[email_attribute]',  | ||||
|                          saml_config['email_attribute'] || 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',  | ||||
|                          class: 'input input-bordered w-full',  | ||||
|                          placeholder: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">SAML attribute name that contains the user's email</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[first_name_attribute]', 'First Name Attribute', class: 'label' %> | ||||
|       <%= text_field_tag 'saml_config[first_name_attribute]',  | ||||
|                          saml_config['first_name_attribute'] || 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',  | ||||
|                          class: 'input input-bordered w-full',  | ||||
|                          placeholder: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">SAML attribute name that contains the user's first name</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="form-control"> | ||||
|       <%= label_tag 'saml_config[last_name_attribute]', 'Last Name Attribute', class: 'label' %> | ||||
|       <%= text_field_tag 'saml_config[last_name_attribute]',  | ||||
|                          saml_config['last_name_attribute'] || 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',  | ||||
|                          class: 'input input-bordered w-full',  | ||||
|                          placeholder: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' %> | ||||
|       <div class="label"> | ||||
|         <span class="label-text-alt">SAML attribute name that contains the user's last name</span> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="divider">Service Provider Information</div> | ||||
|      | ||||
|     <div class="alert"> | ||||
|       <%= svg_icon('info_circle', class: 'w-6 h-6') %> | ||||
|       <div> | ||||
|         <p class="font-bold">Configuration URLs for your Identity Provider</p> | ||||
|         <div class="mt-2 space-y-1 text-sm"> | ||||
|           <p><strong>Assertion Consumer Service URL:</strong></p> | ||||
|           <code class="bg-base-200 px-2 py-1 rounded text-xs"><%= "#{request.base_url}/auth/saml/callback" %></code> | ||||
|            | ||||
|           <p class="mt-2"><strong>SP Metadata URL:</strong></p> | ||||
|           <div class="flex items-center gap-2"> | ||||
|             <code class="bg-base-200 px-2 py-1 rounded text-xs flex-1"><%= "#{request.base_url}/auth/saml/metadata" %></code> | ||||
|             <a href="<%= "#{request.base_url}/auth/saml/metadata" %>"  | ||||
|                class="btn btn-sm btn-outline"  | ||||
|                target="_blank" | ||||
|                title="Download SAML Metadata"> | ||||
|               Download | ||||
|             </a> | ||||
|           </div> | ||||
|            | ||||
|           <p class="mt-2"><strong>SP Entity ID:</strong></p> | ||||
|           <code class="bg-base-200 px-2 py-1 rounded text-xs"><%= saml_config['sp_entity_id'] || 'docuseal' %></code> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="flex gap-2"> | ||||
|       <%= f.submit 'Save SAML Configuration', class: 'btn btn-primary' %> | ||||
|       <% if @encrypted_config.persisted? && @encrypted_config.value.present? %> | ||||
|         <%= link_to 'Test SAML Login', user_saml_omniauth_authorize_path,  | ||||
|                     method: :post,  | ||||
|                     class: 'btn btn-outline', | ||||
|                     data: { turbo: false } %> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   </div> | ||||
| <% end %> | ||||
| @ -0,0 +1,10 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddOmniauthToUsers < ActiveRecord::Migration[7.1] | ||||
|   def change | ||||
|     add_column :users, :provider, :string | ||||
|     add_column :users, :uid, :string | ||||
|      | ||||
|     add_index :users, [:provider, :uid], unique: true | ||||
|   end | ||||
| end | ||||
| @ -0,0 +1,89 @@ | ||||
| services: | ||||
|   app: | ||||
|     depends_on: | ||||
|       postgres: | ||||
|         condition: service_healthy | ||||
|       redis: | ||||
|         condition: service_started | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: Dockerfile.dev | ||||
|     tty: true | ||||
|     stdin_open: true | ||||
|     ports: | ||||
|       - 3000:3000 | ||||
|     volumes: | ||||
|       # Mount the entire codebase for live development | ||||
|       - .:/app | ||||
|       # Preserve node_modules and bundle cache | ||||
|       - /app/node_modules | ||||
|       - bundle_cache:/usr/local/bundle | ||||
|       # Mount data directory | ||||
|       - ./docuseal:/data/docuseal | ||||
|     environment: | ||||
|       - RAILS_ENV=development | ||||
|       - NODE_ENV=development | ||||
|       - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal_development | ||||
|       - REDIS_URL=redis://redis:6379/0 | ||||
|       - FORCE_SSL=false | ||||
|       - SECRET_KEY_BASE=development_secret_key_base_change_in_production | ||||
|     command: > | ||||
|       sh -c " | ||||
|         bundle install && | ||||
|         yarn install && | ||||
|         bundle exec rails db:create db:migrate && | ||||
|         rm -f /app/tmp/pids/server.pid && | ||||
|         bundle exec rails server -b 0.0.0.0 | ||||
|       " | ||||
| 
 | ||||
|   postgres: | ||||
|     image: postgres:15 | ||||
|     volumes: | ||||
|       - postgres_data:/var/lib/postgresql/data | ||||
|     environment: | ||||
|       POSTGRES_USER: postgres | ||||
|       POSTGRES_PASSWORD: postgres | ||||
|       POSTGRES_DB: docuseal_development | ||||
|     ports: | ||||
|       - "5432:5432" | ||||
|     healthcheck: | ||||
|       test: ["CMD-SHELL", "pg_isready -U postgres"] | ||||
|       interval: 5s | ||||
|       timeout: 5s | ||||
|       retries: 5 | ||||
| 
 | ||||
|   redis: | ||||
|     image: redis:7-alpine | ||||
|     ports: | ||||
|       - "6379:6379" | ||||
|     volumes: | ||||
|       - redis_data:/data | ||||
| 
 | ||||
|   # Optional: Sidekiq for background jobs | ||||
|   sidekiq: | ||||
|     depends_on: | ||||
|       postgres: | ||||
|         condition: service_healthy | ||||
|       redis: | ||||
|         condition: service_started | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: Dockerfile.dev | ||||
|     volumes: | ||||
|       - .:/app | ||||
|       - /app/node_modules | ||||
|       - bundle_cache:/usr/local/bundle | ||||
|     environment: | ||||
|       - RAILS_ENV=development | ||||
|       - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal_development | ||||
|       - REDIS_URL=redis://redis:6379/0 | ||||
|     command: > | ||||
|       sh -c " | ||||
|         bundle install && | ||||
|         bundle exec sidekiq | ||||
|       " | ||||
| 
 | ||||
| volumes: | ||||
|   postgres_data: | ||||
|   redis_data: | ||||
|   bundle_cache: | ||||
					Loading…
					
					
				
		Reference in new issue