decidim-suomifi 0.18.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.
@@ -0,0 +1,236 @@
1
+ # Decidim::Suomifi
2
+
3
+ [![Build Status](https://travis-ci.com/mainio/decidim-module-suomifi.svg?branch=master)](https://travis-ci.com/mainio/decidim-module-suomifi)
4
+ [![codecov](https://codecov.io/gh/mainio/decidim-module-suomifi/branch/master/graph/badge.svg)](https://codecov.io/gh/mainio/decidim-module-suomifi)
5
+
6
+ A [Decidim](https://github.com/decidim/decidim) module to add Suomi.fi
7
+ strong authentication to Decidim as a way to authenticate and authorize the
8
+ users.
9
+
10
+ The gem has been developed by [Mainio Tech](https://www.mainiotech.fi/).
11
+
12
+ The development has been sponsored by the
13
+ [City of Helsinki](https://www.hel.fi/).
14
+
15
+ The Population Register Centre (VRK) or the Suomi.fi maintainers are not related
16
+ to this gem in any way, nor do they provide technical support for it. Please
17
+ contact the gem maintainers in case you find any issues with it.
18
+
19
+ ## Preparation
20
+
21
+ Please refer to the
22
+ [`omniauth-suomifi`](https://github.com/mainio/omniauth-suomifi) documentation
23
+ in order to learn more about the preparation and getting started with Suomi.fi.
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem "decidim-suomifi"
31
+ ```
32
+
33
+ And then execute:
34
+
35
+ ```bash
36
+ $ bundle
37
+ ```
38
+
39
+ After installation, you can add the initializer running the following command:
40
+
41
+ ```bash
42
+ $ bundle exec rails generate decidim:suomifi:install
43
+ ```
44
+
45
+ You need to set the following configuration options inside the initializer:
46
+
47
+ - `:scope_of_data` - The scope of data for Suomi.fi
48
+ * Default: `:medium_extensive`
49
+ * `:limited` - Limited scope (name and personal identity number)
50
+ * `:medium_extensive` - Medium extensive scope (limted + address information)
51
+ * `:extensive` - Extensive scope (medium extensive + Finnish citizenship
52
+ information)
53
+ - `:sp_entity_id` - The service provider entity ID, i.e. your applications
54
+ entity ID used to identify the service at the Suomi.fi SAML identity provider.
55
+ * Set this to the same ID that you use for the metadata sent to Suomi.fi.
56
+ * Default: depends on the application's URL, e.g.
57
+ `https://www.example.org/users/auth/suomifi/metadata`
58
+ - `:certificate_file` - Path to the local certificate included in the metadata
59
+ sent to Suomi.fi.
60
+ - `:private_key_file` - Path to the local private key (corresponding to the
61
+ certificate). Will be used to decrypt messages coming from Suomi.fi.
62
+ - `:auto_email_domain` - Defines the auto-email domain in case the user's domain
63
+ is not stored at Suomi.fi. In case this is not set (default), emails will not
64
+ be auto-generated and users will need to enter them manually in case Suomi.fi
65
+ does not report them.
66
+ * The auto-generated email format is similar to the following string:
67
+ `suomifi-756be91097ac490961fd04f121cb9550@example.org`. The email will
68
+ always have the `suomifi-` prefix and the domain part is defined by the
69
+ configuration option.
70
+
71
+ For more information about these options and possible other options, please
72
+ refer to the [`omniauth-suomifi`](https://github.com/mainio/omniauth-suomifi)
73
+ documentation.
74
+
75
+ Note that you will also need to generate a private key and a corresponding
76
+ certificate and configure them inside the generated initializer. For the testing
77
+ environment you can create a self signed certificate e.g. with the following
78
+ command:
79
+
80
+ ```bash
81
+ $ mkdir config/cert
82
+ $ cd config/cert
83
+ $ openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 \
84
+ -keyout suomifi.key -out suomifi.crt
85
+ ```
86
+
87
+ For the production environment you will need an actual certificate signed by
88
+ a trusted CA. The self signed certificate can be used for the Suomi.fi test
89
+ environment.
90
+
91
+ The install generator will also enable the Suomi.fi authentication method for
92
+ OmniAuth by default by adding these lines your `config/secrets.yml`:
93
+
94
+ ```yml
95
+ default: &default
96
+ # ...
97
+ omniauth:
98
+ # ...
99
+ suomifi:
100
+ enabled: false
101
+ icon: globe
102
+ development:
103
+ # ...
104
+ omniauth:
105
+ # ...
106
+ suomifi:
107
+ enabled: true
108
+ mode: test
109
+ icon: globe
110
+ ```
111
+
112
+ This will enable the Suomi.fi authentication for the development environment
113
+ only. In case you want to enable it for other environments as well, apply the
114
+ OmniAuth configuration keys accordingly to other environments as well.
115
+
116
+ The development environment is hooking into the Suomi.fi testing endpoints by
117
+ default which is defined by the `mode: test` option in the OmniAuth
118
+ configuration. For environments that you want to hook into the Suomi.fi
119
+ production environment, you can omit this configuration option completely.
120
+
121
+ The example configuration will set the `globe` icon for the the authentication
122
+ button from the Decidim's own iconset. In case you want to have a better and
123
+ more formal styling for the sign in button, you will need to customize the sign
124
+ in / sign up views.
125
+
126
+ ## Usage
127
+
128
+ After the installation steps, you will need to enable the Suomi.fi authorization
129
+ from Decidim's system management panel. After enabled, you can start using it.
130
+
131
+ This gem also provides a Suomi.fi sign in method which will automatically
132
+ authorize the user accounts. In case the users already have an account, they
133
+ can still authorize themselves using the Suomi.fi authorization.
134
+
135
+ ## Customization
136
+
137
+ For some specific needs, you may need to store extra metadata for the Suomi.fi
138
+ authorization or add new authorization configuration options for the
139
+ authorization.
140
+
141
+ This can be achieved by applying the following configuration to the module
142
+ inside the initializer described above:
143
+
144
+ ```ruby
145
+ # config/initializers/suomifi.rb
146
+
147
+ Decidim::Suomifi.configure do |config|
148
+ # ... keep the default configuration as is ...
149
+ # Add this extra configuration:
150
+ config.workflow_configurator = lambda do |workflow|
151
+ # When expiration is set to 0 minutes, it will never expire.
152
+ workflow.expires_in = 0.minutes
153
+ workflow.action_authorizer = "CustomSuomifiActionAuthorizer"
154
+ workflow.options do |options|
155
+ options.attribute :custom_option, type: :string, required: false
156
+ end
157
+ end
158
+ config.metadata_collector_class = CustomSuomifiMetadataCollector
159
+ end
160
+ ```
161
+
162
+ For the workflow configuration options, please refer to the
163
+ [decidim-verifications documentation](https://github.com/decidim/decidim/tree/master/decidim-verifications).
164
+
165
+ For the custom metadata collector, please extend the default class as follows:
166
+
167
+ ```ruby
168
+ # frozen_string_literal: true
169
+
170
+ class CustomSuomifiMetadataCollector < Decidim::Suomifi::Verification::MetadataCollector
171
+ def metadata
172
+ base = super
173
+
174
+ base.tap do |data|
175
+ # You can access the SAML attributes using the `saml_attributes` accessor:
176
+ postal_code = saml_attributes[:permanent_domestic_address_postal_code]
177
+
178
+ # Or you can access the base attributes already defined through the
179
+ # default metadata collector:
180
+ postal_code = base[:postal_code]
181
+
182
+ unless postal_code.blank?
183
+ # This will actually add the data to the user's authorization metadata
184
+ # hash.
185
+ data[:extra] = "Custom data for: #{postal_code}"
186
+ end
187
+ end
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## Contributing
193
+
194
+ See [Decidim](https://github.com/decidim/decidim).
195
+
196
+ ### Testing
197
+
198
+ To run the tests run the following in the gem development path:
199
+
200
+ ```bash
201
+ $ bundle
202
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rake test_app
203
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rspec
204
+ ```
205
+
206
+ Note that the database user has to have rights to create and drop a database in
207
+ order to create the dummy test app database.
208
+
209
+ In case you are using [rbenv](https://github.com/rbenv/rbenv) and have the
210
+ [rbenv-vars](https://github.com/rbenv/rbenv-vars) plugin installed for it, you
211
+ can add these environment variables to the root directory of the project in a
212
+ file named `.rbenv-vars`. In this case, you can omit defining these in the
213
+ commands shown above.
214
+
215
+ ### Test code coverage
216
+
217
+ If you want to generate the code coverage report for the tests, you can use
218
+ the `SIMPLECOV=1` environment variable in the rspec command as follows:
219
+
220
+ ```bash
221
+ $ SIMPLECOV=1 bundle exec rspec
222
+ ```
223
+
224
+ This will generate a folder named `coverage` in the project root which contains
225
+ the code coverage report.
226
+
227
+ ### Localization
228
+
229
+ If you would like to see this module in your own language, you can help with its
230
+ translation at Crowdin:
231
+
232
+ https://crowdin.com/project/decidim-suomifi
233
+
234
+ ## License
235
+
236
+ See [LICENSE-AGPLv3.txt](LICENSE-AGPLv3.txt).
@@ -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:suomifi: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:suomifi:install --dummy-cert true")
16
+ end
17
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Suomifi
5
+ class OmniauthCallbacksController < ::Decidim::Devise::OmniauthRegistrationsController
6
+ # Make the view helpers available needed in the views
7
+ helper Decidim::Suomifi::Engine.routes.url_helpers
8
+ helper_method :omniauth_registrations_path
9
+
10
+ skip_before_action :verify_authenticity_token, only: [:suomifi, :failure]
11
+ skip_after_action :verify_same_origin_request, only: [:suomifi, :failure]
12
+
13
+ # This is called always after the user returns from the authentication
14
+ # flow from the Suomi.fi identity provider.
15
+ def suomifi
16
+ if user_signed_in?
17
+ # The user is most likely returning from an authorization request
18
+ # because they are already signed in. In this case, add the
19
+ # authorization and redirect the user back to the authorizations view.
20
+
21
+ # Make sure the user has an identity created in order to aid future
22
+ # Suomi.fi sign ins.
23
+ identity = current_user.identities.find_by(
24
+ organization: current_organization,
25
+ provider: oauth_data[:provider],
26
+ uid: user_identifier
27
+ )
28
+ unless identity
29
+ # Check that the identity is not already bound to another user.
30
+ id = Decidim::Identity.find_by(
31
+ organization: current_organization,
32
+ provider: oauth_data[:provider],
33
+ uid: user_identifier
34
+ )
35
+ return fail_authorize(:identity_bound_to_other_user) if id
36
+
37
+ current_user.identities.create!(
38
+ organization: current_organization,
39
+ provider: oauth_data[:provider],
40
+ uid: user_identifier
41
+ )
42
+ end
43
+
44
+ # Add the authorization for the user
45
+ return fail_authorize unless authorize_user(current_user)
46
+
47
+ # Show the success message and redirect back to the authorizations
48
+ flash[:notice] = t(
49
+ "authorizations.create.success",
50
+ scope: "decidim.suomifi.verification"
51
+ )
52
+ return redirect_to(
53
+ stored_location_for(resource || :user) ||
54
+ decidim_verifications.authorizations_path
55
+ )
56
+ end
57
+
58
+ # Normal authentication request, proceed with Decidim's internal logic.
59
+ send(:create)
60
+ end
61
+
62
+ def failure
63
+ strategy = failed_strategy
64
+ saml_response = strategy.response_object if strategy
65
+ return super unless saml_response
66
+
67
+ # In case we want more info about the returned status codes, use the
68
+ # code below.
69
+ #
70
+ # Status codes:
71
+ # Requester = A problem with the request OR the user cancelled the
72
+ # request at the identity provider.
73
+ # Responder = The handling of the request failed.
74
+ # VersionMismatch = Wrong version in the request.
75
+ #
76
+ # Additional state codes:
77
+ # AuthnFailed = The authentication failed OR the user cancelled
78
+ # the process at the identity provider.
79
+ # RequestDenied = The authenticating endpoint (which the
80
+ # identity provider redirects to) rejected the
81
+ # authentication.
82
+ # if !saml_response.send(:validate_success_status) && !saml_response.status_code.nil?
83
+ # codes = saml_response.status_code.split(" | ").map do |full_code|
84
+ # full_code.split(":").last
85
+ # end
86
+ # end
87
+
88
+ # Some extra validation checks
89
+ validations = [
90
+ # The success status validation fails in case the response status
91
+ # code is something else than "Success". This is most likely because
92
+ # of one the reasons explained above. In general there are few
93
+ # possible explanations for this:
94
+ # 1. The user cancelled the request and returned to the service.
95
+ # 2. The underlying identity service the IdP redirects to rejected
96
+ # the request for one reason or another. E.g. the user cancelled
97
+ # the request at the identity service.
98
+ # 3. There is some technical problem with the identity provider
99
+ # service or the XML request sent to there is malformed.
100
+ :success_status,
101
+ # Checks if the local session should be expired, i.e. if the user
102
+ # took too long time to go through the authorization endpoint.
103
+ :session_expiration,
104
+ # The NotBefore and NotOnOrAfter conditions failed, i.e. whether the
105
+ # request is handled within the allowed timeframe by the IdP.
106
+ :conditions
107
+ ]
108
+ validations.each do |key|
109
+ next if saml_response.send("validate_#{key}")
110
+
111
+ flash[:alert] = t(".#{key}")
112
+ return redirect_to after_omniauth_failure_path_for(resource_name)
113
+ end
114
+
115
+ super
116
+ end
117
+
118
+ # This is overridden method from the Devise controller helpers
119
+ # This is called when the user is successfully authenticated which means
120
+ # that we also need to add the authorization for the user automatically
121
+ # because a succesful Suomi.fi authentication means the user has been
122
+ # successfully authorized as well.
123
+ def sign_in_and_redirect(resource_or_scope, *args)
124
+ # Add authorization for the user
125
+ if resource_or_scope.is_a?(::Decidim::User)
126
+ return fail_authorize unless authorize_user(resource_or_scope)
127
+ end
128
+
129
+ super
130
+ end
131
+
132
+ private
133
+
134
+ def authorize_user(user)
135
+ authorization = Decidim::Authorization.find_by(
136
+ name: "suomifi_eid",
137
+ unique_id: user_signature
138
+ )
139
+ if authorization
140
+ return nil if authorization.user != user
141
+ else
142
+ authorization = Decidim::Authorization.find_or_initialize_by(
143
+ name: "suomifi_eid",
144
+ user: user
145
+ )
146
+ end
147
+
148
+ authorization.attributes = {
149
+ unique_id: user_signature,
150
+ metadata: authorization_metadata
151
+ }
152
+ authorization.save!
153
+
154
+ # This will update the "granted_at" timestamp of the authorization which
155
+ # will postpone expiration on re-authorizations in case the
156
+ # authorization is set to expire (by default it will not expire).
157
+ authorization.grant!
158
+
159
+ authorization
160
+ end
161
+
162
+ def fail_authorize(failure_message_key = :already_authorized)
163
+ flash[:alert] = t(
164
+ "failure.#{failure_message_key}",
165
+ scope: "decidim.suomifi.omniauth_callbacks"
166
+ )
167
+ redirect_to stored_location_for(resource || :user) || decidim.root_path
168
+ end
169
+
170
+ # Data that is stored against the authorization "permanently" (i.e. as
171
+ # long as the authorization is valid).
172
+ def authorization_metadata
173
+ metadata_collector.metadata
174
+ end
175
+
176
+ # Needs to be specifically defined because the core engine routes are not
177
+ # all properly loaded for the view and this helper method is needed for
178
+ # defining the omniauth registration form's submit path.
179
+ def omniauth_registrations_path(resource)
180
+ Decidim::Core::Engine.routes.url_helpers.omniauth_registrations_path(resource)
181
+ end
182
+
183
+ # Private: Create form params from omniauth hash
184
+ # Since we are using trusted omniauth data we are generating a valid signature.
185
+ def user_params_from_oauth_hash
186
+ return nil if oauth_data.empty?
187
+ return nil if saml_attributes.empty?
188
+ return nil if user_identifier.blank?
189
+
190
+ {
191
+ provider: oauth_data[:provider],
192
+ uid: user_identifier,
193
+ name: user_full_name,
194
+ # The nickname is automatically "parametrized" by Decidim core from
195
+ # the name string, i.e. it will be in correct format.
196
+ nickname: user_full_name,
197
+ oauth_signature: user_signature,
198
+ avatar_url: oauth_data[:info][:image],
199
+ raw_data: oauth_hash
200
+ }
201
+ end
202
+
203
+ def user_full_name
204
+ return oauth_data[:info][:name] if oauth_data[:info][:name]
205
+
206
+ @user_full_name ||= begin
207
+ first_name = begin
208
+ saml_attributes[:given_name] ||
209
+ saml_attributes[:first_names] ||
210
+ saml_attributes[:eidas_first_names]
211
+ end
212
+ last_name = begin
213
+ saml_attributes[:last_name] ||
214
+ saml_attributes[:eidas_family_name]
215
+ end
216
+
217
+ "#{first_name} #{last_name}"
218
+ end
219
+ end
220
+
221
+ def user_signature
222
+ @user_signature ||= OmniauthRegistrationForm.create_signature(
223
+ oauth_data[:provider],
224
+ user_identifier
225
+ )
226
+ end
227
+
228
+ # See the omniauth-suomi gem's notes about the UID. It should be always
229
+ # unique per person as long as it can be determined from the user's data.
230
+ # This consists of one of the following in this order:
231
+ # - The person's electronic identifier (SATU ID, sähköinen asiointitunnus)
232
+ # - The person's personal identifier (HETU ID, henkilötunnus) in hashed
233
+ # format
234
+ # - The person's eIDAS personal identifier (eIDAS PID) in hashed format
235
+ # - The SAML NameID in the SAML response in case no unique personal data
236
+ # is available as defined above
237
+ def user_identifier
238
+ @user_identifier ||= oauth_data[:uid]
239
+ end
240
+
241
+ def person_identifier_digest
242
+ metadata_collector.person_identifier_digest
243
+ end
244
+
245
+ def metadata_collector
246
+ @metadata_collector ||= Decidim::Suomifi::Verification::Manager.metadata_collector_for(
247
+ saml_attributes
248
+ )
249
+ end
250
+
251
+ def verified_email
252
+ @verified_email ||= begin
253
+ if saml_attributes[:email]
254
+ saml_attributes[:email]
255
+ elsif Decidim::Suomifi.auto_email_domain
256
+ domain = Decidim::Suomifi.auto_email_domain
257
+ "suomifi-#{person_identifier_digest}@#{domain}"
258
+ end
259
+ end
260
+ end
261
+
262
+ def saml_attributes
263
+ @saml_attributes ||= oauth_hash[:extra][:saml_attributes]
264
+ end
265
+ end
266
+ end
267
+ end