decidim-msad 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +476 -0
  4. data/Rakefile +17 -0
  5. data/app/controllers/decidim/msad/omniauth_callbacks_controller.rb +178 -0
  6. data/app/controllers/decidim/msad/sessions_controller.rb +58 -0
  7. data/app/controllers/decidim/msad/verification/authorizations_controller.rb +19 -0
  8. data/config/locales/en.yml +27 -0
  9. data/config/locales/fi.yml +26 -0
  10. data/config/locales/sv.yml +26 -0
  11. data/lib/decidim/msad.rb +198 -0
  12. data/lib/decidim/msad/authentication.rb +4 -0
  13. data/lib/decidim/msad/authentication/authenticator.rb +229 -0
  14. data/lib/decidim/msad/authentication/errors.rb +21 -0
  15. data/lib/decidim/msad/engine.rb +112 -0
  16. data/lib/decidim/msad/mail_interceptors.rb +9 -0
  17. data/lib/decidim/msad/mail_interceptors/generated_recipients_interceptor.rb +25 -0
  18. data/lib/decidim/msad/test/runtime.rb +38 -0
  19. data/lib/decidim/msad/verification.rb +5 -0
  20. data/lib/decidim/msad/verification/engine.rb +43 -0
  21. data/lib/decidim/msad/verification/manager.rb +17 -0
  22. data/lib/decidim/msad/verification/metadata_collector.rb +39 -0
  23. data/lib/decidim/msad/version.rb +8 -0
  24. data/lib/generators/decidim/msad/install_generator.rb +126 -0
  25. data/lib/generators/templates/msad_initializer.rb +74 -0
  26. data/lib/generators/templates/msad_initializer_test.rb +5 -0
  27. data/lib/omniauth/msad/metadata.rb +46 -0
  28. data/lib/omniauth/msad/settings.rb +9 -0
  29. data/lib/omniauth/msad/test.rb +10 -0
  30. data/lib/omniauth/msad/test/certificate_generator.rb +51 -0
  31. data/lib/omniauth/strategies/msad.rb +187 -0
  32. metadata +116 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/dev/common_rake"
4
+
5
+ desc "Generates a dummy app for testing"
6
+ task test_app: "decidim:generate_external_test_app" do
7
+ Dir.chdir("spec/decidim_dummy_app") do
8
+ system("bundle exec rails generate decidim:msad:install --test-initializer true")
9
+ end
10
+ end
11
+
12
+ desc "Generates a development app."
13
+ task development_app: "decidim:generate_external_development_app" do
14
+ Dir.chdir("development_app") do
15
+ system("bundle exec rails generate decidim:msad:install")
16
+ end
17
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Msad
5
+ class OmniauthCallbacksController < ::Decidim::Devise::OmniauthRegistrationsController
6
+ # Make the view helpers available needed in the views
7
+ helper Decidim::Msad::Engine.routes.url_helpers
8
+ helper_method :omniauth_registrations_path
9
+
10
+ skip_before_action :verify_authenticity_token, only: [:msad, :failure]
11
+ skip_after_action :verify_same_origin_request, only: [:msad, :failure]
12
+
13
+ # This is called always after the user returns from the authentication
14
+ # flow from the Active Directory identity provider.
15
+ def msad
16
+ session["decidim-msad.signed_in"] = true
17
+
18
+ authenticator.validate!
19
+
20
+ if user_signed_in?
21
+ # The user is most likely returning from an authorization request
22
+ # because they are already signed in. In this case, add the
23
+ # authorization and redirect the user back to the authorizations view.
24
+
25
+ # Make sure the user has an identity created in order to aid future
26
+ # Active Directory sign ins. In case this fails, it will raise a
27
+ # Decidim::Msad::Authentication::IdentityBoundToOtherUserError
28
+ # which is handled below.
29
+ authenticator.identify_user!(current_user)
30
+
31
+ # Add the authorization for the user
32
+ return fail_authorize unless authorize_user(current_user)
33
+
34
+ # Make sure the user details are up to date
35
+ authenticator.update_user!(current_user)
36
+
37
+ # Show the success message and redirect back to the authorizations
38
+ flash[:notice] = t(
39
+ "authorizations.create.success",
40
+ scope: "decidim.msad.verification"
41
+ )
42
+ return redirect_to(
43
+ stored_location_for(resource || :user) ||
44
+ decidim_verifications.authorizations_path
45
+ )
46
+ end
47
+
48
+ # Normal authentication request, proceed with Decidim's internal logic.
49
+ send(:create)
50
+ rescue Decidim::Msad::Authentication::ValidationError => e
51
+ fail_authorize(e.validation_key)
52
+ rescue Decidim::Msad::Authentication::IdentityBoundToOtherUserError
53
+ fail_authorize(:identity_bound_to_other_user)
54
+ end
55
+
56
+ def failure
57
+ strategy = failed_strategy
58
+ saml_response = strategy.response_object if strategy
59
+ return super unless saml_response
60
+
61
+ # In case we want more info about the returned status codes, use the
62
+ # code below.
63
+ #
64
+ # Status codes:
65
+ # Requester = A problem with the request OR the user cancelled the
66
+ # request at the identity provider.
67
+ # Responder = The handling of the request failed.
68
+ # VersionMismatch = Wrong version in the request.
69
+ #
70
+ # Additional state codes:
71
+ # AuthnFailed = The authentication failed OR the user cancelled
72
+ # the process at the identity provider.
73
+ # RequestDenied = The authenticating endpoint (which the
74
+ # identity provider redirects to) rejected the
75
+ # authentication.
76
+ # if !saml_response.send(:validate_success_status) && !saml_response.status_code.nil?
77
+ # codes = saml_response.status_code.split(" | ").map do |full_code|
78
+ # full_code.split(":").last
79
+ # end
80
+ # end
81
+
82
+ # Some extra validation checks
83
+ validations = [
84
+ # The success status validation fails in case the response status
85
+ # code is something else than "Success". This is most likely because
86
+ # of one the reasons explained above. In general there are few
87
+ # possible explanations for this:
88
+ # 1. The user cancelled the request and returned to the service.
89
+ # 2. The underlying identity service the IdP redirects to rejected
90
+ # the request for one reason or another. E.g. the user cancelled
91
+ # the request at the identity service.
92
+ # 3. There is some technical problem with the identity provider
93
+ # service or the XML request sent to there is malformed.
94
+ :success_status,
95
+ # Checks if the local session should be expired, i.e. if the user
96
+ # took too long time to go through the authorization endpoint.
97
+ :session_expiration,
98
+ # The NotBefore and NotOnOrAfter conditions failed, i.e. whether the
99
+ # request is handled within the allowed timeframe by the IdP.
100
+ :conditions
101
+ ]
102
+ validations.each do |key|
103
+ next if saml_response.send("validate_#{key}")
104
+
105
+ flash[:alert] = t(".#{key}")
106
+ return redirect_to after_omniauth_failure_path_for(resource_name)
107
+ end
108
+
109
+ super
110
+ end
111
+
112
+ # This is overridden method from the Devise controller helpers
113
+ # This is called when the user is successfully authenticated which means
114
+ # that we also need to add the authorization for the user automatically
115
+ # because a succesful Active Directory authentication means the user has
116
+ # been successfully authorized as well.
117
+ def sign_in_and_redirect(resource_or_scope, *args)
118
+ # Add authorization for the user
119
+ if resource_or_scope.is_a?(::Decidim::User)
120
+ return fail_authorize unless authorize_user(resource_or_scope)
121
+
122
+ # Make sure the user details are up to date
123
+ authenticator.update_user!(resource_or_scope)
124
+ end
125
+
126
+ super
127
+ end
128
+
129
+ private
130
+
131
+ def authorize_user(user)
132
+ authenticator.authorize_user!(user)
133
+ rescue Decidim::Msad::Authentication::AuthorizationBoundToOtherUserError
134
+ nil
135
+ end
136
+
137
+ def fail_authorize(failure_message_key = :already_authorized)
138
+ flash[:alert] = t(
139
+ "failure.#{failure_message_key}",
140
+ scope: "decidim.msad.omniauth_callbacks"
141
+ )
142
+
143
+ redirect_path = stored_location_for(resource || :user) || decidim.root_path
144
+ if session.delete("decidim-msad.signed_in")
145
+ params = "?RelayState=#{CGI.escape(redirect_path)}"
146
+
147
+ return redirect_to user_msad_omniauth_spslo_path + params
148
+ end
149
+
150
+ redirect_to redirect_path
151
+ end
152
+
153
+ # Needs to be specifically defined because the core engine routes are not
154
+ # all properly loaded for the view and this helper method is needed for
155
+ # defining the omniauth registration form's submit path.
156
+ def omniauth_registrations_path(resource)
157
+ Decidim::Core::Engine.routes.url_helpers.omniauth_registrations_path(resource)
158
+ end
159
+
160
+ # Private: Create form params from omniauth hash
161
+ # Since we are using trusted omniauth data we are generating a valid signature.
162
+ def user_params_from_oauth_hash
163
+ authenticator.user_params_from_oauth_hash
164
+ end
165
+
166
+ def authenticator
167
+ @authenticator ||= Decidim::Msad.authenticator_for(
168
+ current_organization,
169
+ oauth_hash
170
+ )
171
+ end
172
+
173
+ def verified_email
174
+ authenticator.verified_email
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Msad
5
+ class SessionsController < ::Decidim::Devise::SessionsController
6
+ def destroy
7
+ # In case the user is signed in through the AD federation server,
8
+ # redirect them through the SPSLO flow.
9
+ if session.delete("decidim-msad.signed_in")
10
+ # These session variables get destroyed along with the user's active
11
+ # session. They are needed for the SLO request.
12
+ saml_uid = session["saml_uid"]
13
+ saml_session_index = session["saml_session_index"]
14
+ stored_location = stored_location_for(resource_name)
15
+
16
+ # End the local user session.
17
+ signed_out = (::Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
18
+
19
+ # Store the SAML parameters for the SLO request utilized by
20
+ # omniauth-saml. These are used to generate a valid SLO request.
21
+ session["saml_uid"] = saml_uid
22
+ session["saml_session_index"] = saml_session_index
23
+ store_location_for(resource_name, stored_location) if stored_location
24
+
25
+ # Generate the SLO redirect path and parameters.
26
+ relay = slo_callback_user_session_path
27
+ relay += "?success=1" if signed_out
28
+ params = "?RelayState=#{CGI.escape(relay)}"
29
+
30
+ return redirect_to user_msad_omniauth_spslo_path + params
31
+ end
32
+
33
+ # Otherwise, continue normally
34
+ super
35
+ end
36
+
37
+ def slo
38
+ # This is handled already by omniauth
39
+ redirect_to decidim.root_path
40
+ end
41
+
42
+ def spslo
43
+ # This is handled already by omniauth
44
+ redirect_to decidim.root_path
45
+ end
46
+
47
+ def slo_callback
48
+ set_flash_message! :notice, :signed_out if params[:success] == "1"
49
+
50
+ # Redirect to the root path when the organization forces users to
51
+ # authenticate before accessing the organization.
52
+ return redirect_to(decidim.new_user_session_path) if current_organization.force_users_to_authenticate_before_access_organization
53
+
54
+ redirect_to stored_location_for(resource_name) || decidim.root_path
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Msad
5
+ module Verification
6
+ class AuthorizationsController < ::Decidim::ApplicationController
7
+ skip_before_action :store_current_location
8
+
9
+ def new
10
+ # Do not enforce the permission here because it would cause
11
+ # re-authorizations not to work as the authorization already exists.
12
+ # In case the user wants to re-authorize themselves, they can just
13
+ # hit this endpoint again.
14
+ redirect_to decidim.user_msad_omniauth_authorize_path
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ ---
2
+ en:
3
+ decidim:
4
+ authorization_handlers:
5
+ msad_identity:
6
+ explanation: Identify yourself using the Active Directory identity service.
7
+ name: Active Directory identity
8
+ msad:
9
+ omniauth_callbacks:
10
+ failure:
11
+ already_authorized: Another user has already authorized themselves with the same identity.
12
+ conditions: The authentication request was not handled within an allowed timeframe. Please try again.
13
+ identity_bound_to_other_user: Another user has already been identified using this identity. Please sign out and sign in again directly using Active Directory.
14
+ invalid_data: You cannot be authenticated through Active Directory.
15
+ session_expiration: Authentication session expired. Please try again.
16
+ success_status: Authentication failed or cancelled. Please try again.
17
+ verification:
18
+ authorizations:
19
+ create:
20
+ success: You have been successfully authorized through Active Directory
21
+ destroy:
22
+ success: Authorization sucessfully reset.
23
+ system:
24
+ organizations:
25
+ omniauth_settings:
26
+ msad:
27
+ metadata_url: Metadata URL
@@ -0,0 +1,26 @@
1
+ fi:
2
+ decidim:
3
+ authorization_handlers:
4
+ msad_identity:
5
+ explanation: Tunnista itsesi Active Directory -tunnistuspalvelun avulla.
6
+ name: Active Directory tunnistus
7
+ msad:
8
+ omniauth_callbacks:
9
+ failure:
10
+ already_authorized: Toinen käyttäjä on tunnistanut itsensä jo samalla henkilöllisyydellä.
11
+ conditions: Tunnistuspyyntöä ei käsitelty sallitun aikarajan sisällä. Yritä uudestaan.
12
+ identity_bound_to_other_user: Toinen käyttäjä on jo tunnistanut itsensä tällä henkilöllisyydellä. Kirjaudu ulos ja kirjaudu uudestaan sisään käyttäen suoraan Active Directory -tunnistusta.
13
+ invalid_data: Sinua ei voida tunnistaa Active Directoryn kautta.
14
+ session_expiration: Tunnistusistunto vanhentui. Yritä uudestaan.
15
+ success_status: Tunnistus epäonnistui tai peruutettiin. Yritä uudestaan.
16
+ verification:
17
+ authorizations:
18
+ create:
19
+ success: Sinut on onnistuneesti tunnistettu Active Directory -palvelun avulla
20
+ destroy:
21
+ success: Varmennus tyhjennetty onnistuneesti.
22
+ system:
23
+ organizations:
24
+ omniauth_settings:
25
+ msad:
26
+ metadata_url: Metadatan osoite (URL)
@@ -0,0 +1,26 @@
1
+ sv:
2
+ decidim:
3
+ authorization_handlers:
4
+ msad_identity:
5
+ explanation: Identifiera dig själv med Active Directory -identifikation.
6
+ name: Active Directory -identifikation
7
+ msad:
8
+ omniauth_callbacks:
9
+ failure:
10
+ already_authorized: En annan användare har redan godkänt sig med samma identitet.
11
+ conditions: Autentiseringsbegäran hanterades inte inom en tillåten tidsram. Var god försök igen.
12
+ identity_bound_to_other_user: En annan användare har redan identifierats med denna identitet. Logga ut och logga in igen direkt med Active Directory.
13
+ invalid_data: Du kan inte verifieras via Active Directory.
14
+ session_expiration: Autentiseringssessionen har gått ut. Var god försök igen.
15
+ success_status: Autentiseringen misslyckades eller avbröts. Var god försök igen.
16
+ verification:
17
+ authorizations:
18
+ create:
19
+ success: Du har godkänts med Active Directory
20
+ destroy:
21
+ success: Tillståndet återställs efterhand.
22
+ system:
23
+ organizations:
24
+ omniauth_settings:
25
+ msad:
26
+ metadata_url: Metadata URL
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth"
4
+ require "omniauth/strategies/msad"
5
+
6
+ require_relative "msad/version"
7
+ require_relative "msad/engine"
8
+ require_relative "msad/authentication"
9
+ require_relative "msad/verification"
10
+ require_relative "msad/mail_interceptors"
11
+
12
+ module Decidim
13
+ module Msad
14
+ include ActiveSupport::Configurable
15
+
16
+ @configured = false
17
+
18
+ # Defines the auto email domain to generate verified email addresses upon
19
+ # the user's registration automatically that have format similar to
20
+ # "msad-identifier@auto-email-domain.fi".
21
+ #
22
+ # In case this is not defined, the default is the organization's domain.
23
+ config_accessor :auto_email_domain
24
+
25
+ config_accessor :idp_metadata_url
26
+ config_accessor :sp_entity_id, instance_reader: false
27
+
28
+ # The certificate string for the application
29
+ config_accessor :certificate, instance_reader: false
30
+
31
+ # The private key string for the application
32
+ config_accessor :private_key, instance_reader: false
33
+
34
+ # The certificate file for the application
35
+ config_accessor :certificate_file
36
+
37
+ # The private key file for the application
38
+ config_accessor :private_key_file
39
+
40
+ # Defines how the session gets cleared when the OmniAuth strategy logs the
41
+ # user out. This has been customized to preserve the flash messages and the
42
+ # stored redirect location in the session after the session is destroyed.
43
+ config_accessor :idp_slo_session_destroy do
44
+ proc do |_env, session|
45
+ flash = session["flash"]
46
+ return_to = session["user_return_to"]
47
+ result = session.clear
48
+ session["flash"] = flash if flash
49
+ session["user_return_to"] = return_to if return_to
50
+ result
51
+ end
52
+ end
53
+
54
+ # These are extra attributes that can be stored for the authorization
55
+ # metadata. Define these as follows:
56
+ #
57
+ # Decidim::Msad.configure do |config|
58
+ # # ...
59
+ # config.metadata_attributes = {
60
+ # primary_group_sid: "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarygroupsid",
61
+ # groups: { name: "http://schemas.xmlsoap.org/claims/Group", type: :multi }
62
+ # }
63
+ # end
64
+ config_accessor :metadata_attributes do
65
+ {}
66
+ end
67
+
68
+ # Extra metadata to be included in the service provider metadata. Define as
69
+ # follows:
70
+ #
71
+ # Decidim::Msad.configure do |config|
72
+ # # ...
73
+ # config.sp_metadata = [
74
+ # {
75
+ # name: "Organization",
76
+ # children: [
77
+ # {
78
+ # name: "OrganizationName",
79
+ # attributes: { "xml:lang" => "en-US" },
80
+ # content: "Acme"
81
+ # }
82
+ # ]
83
+ # }
84
+ # ]
85
+ # end
86
+ config_accessor :sp_metadata do
87
+ []
88
+ end
89
+
90
+ # Extra configuration for the omniauth strategy
91
+ config_accessor :extra do
92
+ {}
93
+ end
94
+
95
+ # Defines whether registered users are automatically subscribed to the
96
+ # newsletters during the OmniAuth registration flow. This is only updated
97
+ # during the first login, so users can still unsubscribe if they later
98
+ # decide they don't want to receive the newsletter and later logins will not
99
+ # change the subscription state.
100
+ config_accessor :registration_newsletter_subscriptions do
101
+ false
102
+ end
103
+
104
+ # Allows customizing the authorization workflow e.g. for adding custom
105
+ # workflow options or configuring an action authorizer for the
106
+ # particular needs.
107
+ config_accessor :workflow_configurator do
108
+ lambda do |workflow|
109
+ # By default, expiration is set to 0 minutes which means it will
110
+ # never expire.
111
+ workflow.expires_in = 0.minutes
112
+ end
113
+ end
114
+
115
+ # Allows customizing parts of the authentication flow such as validating
116
+ # the authorization data before allowing the user to be authenticated.
117
+ config_accessor :authenticator_class do
118
+ Decidim::Msad::Authentication::Authenticator
119
+ end
120
+
121
+ # Allows customizing how the authorization metadata gets collected from
122
+ # the SAML attributes passed from the authorization endpoint.
123
+ config_accessor :metadata_collector_class do
124
+ Decidim::Msad::Verification::MetadataCollector
125
+ end
126
+
127
+ def self.configured?
128
+ @configured
129
+ end
130
+
131
+ def self.configure
132
+ @configured = true
133
+ super
134
+ end
135
+
136
+ def self.authenticator_for(organization, oauth_hash)
137
+ authenticator_class.new(organization, oauth_hash)
138
+ end
139
+
140
+ def self.sp_entity_id
141
+ return config.sp_entity_id if config.sp_entity_id
142
+
143
+ "#{application_host}/users/auth/msad/metadata"
144
+ end
145
+
146
+ def self.certificate
147
+ return File.read(certificate_file) if certificate_file
148
+
149
+ config.certificate
150
+ end
151
+
152
+ def self.private_key
153
+ return File.read(private_key_file) if private_key_file
154
+
155
+ config.private_key
156
+ end
157
+
158
+ def self.omniauth_settings
159
+ {
160
+ idp_metadata_url: idp_metadata_url,
161
+ sp_entity_id: sp_entity_id,
162
+ sp_name_qualifier: sp_entity_id,
163
+ idp_slo_session_destroy: idp_slo_session_destroy,
164
+ sp_metadata: sp_metadata,
165
+ certificate: certificate,
166
+ private_key: private_key,
167
+ # Define the assertion and SLO URLs for the metadata.
168
+ assertion_consumer_service_url: "#{application_host}/users/auth/msad/callback",
169
+ single_logout_service_url: "#{application_host}/users/auth/msad/slo"
170
+ }.merge(extra)
171
+ end
172
+
173
+ # Used to determine the default service provider entity ID in case not
174
+ # specifically set by the `sp_entity_id` configuration option.
175
+ def self.application_host
176
+ conf = Rails.application.config
177
+ url_options = conf.action_controller.default_url_options
178
+ url_options = conf.action_mailer.default_url_options if !url_options || !url_options[:host]
179
+ url_options ||= {}
180
+
181
+ # Note that at least Azure AD requires all callback URLs to be HTTPS, so
182
+ # we'll default to that.
183
+ host = url_options[:host]
184
+ port = url_options[:port]
185
+ protocol = url_options[:protocol]
186
+ protocol = port.to_i == 80 ? "http" : "https" if protocol.blank?
187
+ if host.blank?
188
+ # Default to local development environment.
189
+ host = "localhost"
190
+ port ||= 3000
191
+ end
192
+
193
+ return "#{protocol}://#{host}:#{port}" if port && ![80, 443].include?(port.to_i)
194
+
195
+ "#{protocol}://#{host}"
196
+ end
197
+ end
198
+ end