decidim-suomifi 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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