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