Browse Source

Fixes issue #2741: Authenticate users via SAML.

Thorsten Eckel 5 years ago
parent
commit
a93250e210

+ 1 - 0
Gemfile

@@ -74,6 +74,7 @@ gem 'omniauth-google-oauth2'
 gem 'omniauth-linkedin-oauth2'
 gem 'omniauth-microsoft-office365'
 gem 'omniauth-oauth2'
+gem 'omniauth-saml'
 gem 'omniauth-twitter'
 gem 'omniauth-weibo-oauth2'
 

+ 6 - 0
Gemfile.lock

@@ -345,6 +345,9 @@ GEM
     omniauth-rails_csrf_protection (0.1.2)
       actionpack (>= 4.2)
       omniauth (>= 1.3.1)
+    omniauth-saml (1.10.1)
+      omniauth (~> 1.3, >= 1.3.2)
+      ruby-saml (~> 1.7)
     omniauth-twitter (1.4.0)
       omniauth-oauth (~> 1.1)
       rack
@@ -456,6 +459,8 @@ GEM
     rubocop-rspec (1.33.0)
       rubocop (>= 0.60.0)
     ruby-progressbar (1.10.1)
+    ruby-saml (1.10.2)
+      nokogiri (>= 1.5.10)
     ruby_dep (1.5.0)
     rubyzip (1.2.2)
     safe_yaml (1.0.5)
@@ -610,6 +615,7 @@ DEPENDENCIES
   omniauth-microsoft-office365
   omniauth-oauth2
   omniauth-rails_csrf_protection
+  omniauth-saml
   omniauth-twitter
   omniauth-weibo-oauth2
   pg (= 0.21.0)

+ 5 - 0
app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee

@@ -106,4 +106,9 @@ App.Config.set('auth_provider_all', {
     name:   'Weibo'
     config: 'auth_weibo'
     class:  'weibo'
+  saml:
+    url:    '/auth/saml'
+    name:   'SAML'
+    config: 'auth_saml'
+    class:  'saml'
 })

+ 2 - 2
app/assets/javascripts/app/views/layout_ref/ui.jst.eco

@@ -72,7 +72,7 @@
       <th>Icon</th>
       <th>Name</th>
     </tr>
-      <% for icon in ['archived-modifier','arrow-down','arrow-left','arrow-right','arrow-up','bold','chain','chat','checkbox-checked','checkbox-indeterminate','checkbox','checkmark','clipboard','clock','cloud','cog','crown','danger','dashboard','diagonal-cross','document','download','draft-modifier','draggable','dropdown-list','email-button','email','external','eye','eyedropper','facebook-button','facebook','file-archive','file-code','file-email','file-excel','file-pdf','file-powerpoint','file-text','file-unknown','file-word','form','forward','full-logo','github-button','gitlab-button','google-button','group','help','horizontal-rule','important','in-process','inactive-organization','inactive-user','info','internal-modifier','italic','knowledge-base-answer','knowledge-base','line-left-arrow','line-right-arrow','linkedin-button','list','loading','lock-open','lock','logo','logotype','long-arrow-down','long-arrow-right','low-priority','magnifier','marker','message','minus-small','minus','mood-bad','mood-good','mood-ok','mood-sad','mood-superbad','mood-supergood','mute','note','oauth2-button','office365-button','one-ticket','organization','outbound-calls','overflow-button','overviews','package','paperclip','pen','person','phone','plus-small','plus','printer','radio-checked','radio','rearange','received-calls','reload','reopening','reply-all','reply','report','searchdetail','signout','small-dot','sms','spinner-small','split','status-modified-outer-circle','status','stopwatch','strikethrough','switchView','task-state','team','telegram','templates','tools','total-tickets','trash','twitter-button','twitter','underline','unmute','unordered-list','user','web','weibo-button','zoom-in','zoom-out']: %>
+      <% for icon in ['archived-modifier','arrow-down','arrow-left','arrow-right','arrow-up','bold','chain','chat','checkbox-checked','checkbox-indeterminate','checkbox','checkmark','clipboard','clock','cloud','cog','crown','danger','dashboard','diagonal-cross','document','download','draft-modifier','draggable','dropdown-list','email-button','email','external','eye','eyedropper','facebook-button','facebook','file-archive','file-code','file-email','file-excel','file-pdf','file-powerpoint','file-text','file-unknown','file-word','form','forward','full-logo','github-button','gitlab-button','google-button','group','help','horizontal-rule','important','in-process','inactive-organization','inactive-user','info','internal-modifier','italic','knowledge-base-answer','knowledge-base','line-left-arrow','line-right-arrow','linkedin-button','list','loading','lock-open','lock','logo','logotype','long-arrow-down','long-arrow-right','low-priority','magnifier','marker','message','minus-small','minus','mood-bad','mood-good','mood-ok','mood-sad','mood-superbad','mood-supergood','mute','note','oauth2-button','office365-button','one-ticket','organization','outbound-calls','overflow-button','overviews','package','paperclip','pen','person','phone','plus-small','plus','printer','radio-checked','radio','rearange','received-calls','reload','reopening','reply-all','reply','report','saml-button','searchdetail','signout','small-dot','sms','spinner-small','split','status-modified-outer-circle','status','stopwatch','strikethrough','switchView','task-state','team','telegram','templates','tools','total-tickets','trash','twitter-button','twitter','underline','unmute','unordered-list','user','web','saml-button','zoom-in','zoom-out']: %>
         <tr>
           <td>
               <%- @Icon( "#{icon}") %>
@@ -84,4 +84,4 @@
       <% end %>
   </table>
 
-</div>
+</div>

+ 4 - 0
app/assets/stylesheets/zammad.scss

@@ -3081,6 +3081,10 @@ ol.tabs li {
     background: hsl(0,0%,27%);
   }
 
+  &.auth-provider--saml {
+    background: hsl(0,0%,27%);
+  }
+
   .provider-name {
     flex: 1;
   }

+ 2 - 0
config/initializers/omniauth.rb

@@ -49,6 +49,8 @@ Rails.application.config.middleware.use OmniAuth::Builder do
   # weibo database connect
   provider :weibo_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
 
+  # SAML database connect
+  provider :saml_database
 end
 
 # This fixes issue #1642 and is required for setups in which Zammad is used

+ 79 - 0
db/migrate/20190715141227_saml_auth.rb

@@ -0,0 +1,79 @@
+class SamlAuth < ActiveRecord::Migration[5.2]
+  def up
+    # return if it's a new setup
+    return if !Setting.find_by(name: 'system_init_done')
+
+    Setting.create_if_not_exists(
+      title:       'Authentication via %s',
+      name:        'auth_saml',
+      area:        'Security::ThirdPartyAuthentication',
+      description: 'Enables user authentication via %s.',
+      options:     {
+        form: [
+          {
+            display: '',
+            null:    true,
+            name:    'auth_saml',
+            tag:     'boolean',
+            options: {
+              true  => 'yes',
+              false => 'no',
+            },
+          },
+        ],
+      },
+      preferences: {
+        controller:       'SettingsAreaSwitch',
+        sub:              ['auth_saml_credentials'],
+        title_i18n:       ['SAML'],
+        description_i18n: ['SAML'],
+        permission:       ['admin.security'],
+      },
+      state:       false,
+      frontend:    true
+    )
+    Setting.create_if_not_exists(
+      title:       'SAML App Credentials',
+      name:        'auth_saml_credentials',
+      area:        'Security::ThirdPartyAuthentication::SAML',
+      description: 'Enables user authentication via SAML.',
+      options:     {
+        form: [
+          {
+            display:     'IDP SSO target URL',
+            null:        true,
+            name:        'idp_sso_target_url',
+            tag:         'input',
+            placeholder: 'https://capriza.github.io/samling/samling.html',
+          },
+          {
+            display:     'IDP certificate',
+            null:        true,
+            name:        'idp_cert',
+            tag:         'input',
+            placeholder: '-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----',
+          },
+          {
+            display:     'IDP certificate fingerprint',
+            null:        true,
+            name:        'idp_cert_fingerprint',
+            tag:         'input',
+            placeholder: 'E7:91:B2:E1:...',
+          },
+          {
+            display:     'Name Identifier Format',
+            null:        true,
+            name:        'name_identifier_format',
+            tag:         'input',
+            placeholder: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+          },
+        ],
+      },
+      state:       {},
+      preferences: {
+        permission: ['admin.security'],
+      },
+      frontend:    false
+    )
+  end
+end

+ 26 - 0
vendor/lib/saml_database.rb

@@ -0,0 +1,26 @@
+class SamlDatabase < OmniAuth::Strategies::SAML
+  option :name, 'saml'
+
+  def initialize(app, *args, &block)
+
+    http_type = Setting.get('http_type')
+    fqdn      = Setting.get('fqdn')
+
+    # Use meta URL as entity id/issues as it is best practice.
+    # See: https://community.zammad.org/t/saml-oidc-third-party-authentication/2533/13
+    entity_id                      = "#{http_type}://#{fqdn}/auth/saml/metadata"
+    assertion_consumer_service_url = "#{http_type}://#{fqdn}/auth/saml/callback"
+
+    config  = Setting.get('auth_saml_credentials') || {}
+    options = config.reject { |k,v| v.blank? }
+                     .merge(
+                        :assertion_consumer_service_url => assertion_consumer_service_url,
+                        :issuer                         => entity_id,
+                      )
+
+    args[0] = options
+
+    super
+  end
+
+end