decidim-mpassid 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,207 @@
1
+ # Decidim::Mpassid
2
+
3
+ [![Build Status](https://travis-ci.com/mainio/decidim-module-mpassid.svg?branch=master)](https://travis-ci.com/mainio/decidim-module-mpassid)
4
+ [![codecov](https://codecov.io/gh/mainio/decidim-module-mpassid/branch/master/graph/badge.svg)](https://codecov.io/gh/mainio/decidim-module-mpassid)
5
+
6
+ A [Decidim](https://github.com/decidim/decidim) module to add MPASSid
7
+ authentication to Decidim as a way to authenticate and authorize the users.
8
+
9
+ The gem has been developed by [Mainio Tech](https://www.mainiotech.fi/).
10
+
11
+ The development has been sponsored by the
12
+ [City of Helsinki](https://www.hel.fi/).
13
+
14
+ The MPASSid service is owned by the Ministry of the Education and Culture and
15
+ operated by CSC - Tieteen tietotekniikan keskus Oy. Neither of these parties or
16
+ the MPASSid maintainers are related to this gem in any way, nor do they provide
17
+ technical support for it. Please contact the gem maintainers in case you find
18
+ any issues with it.
19
+
20
+ ## Preparation
21
+
22
+ Please refer to the
23
+ [`omniauth-mpassid`](https://github.com/mainio/omniauth-mpassid) documentation
24
+ in order to learn more about the preparation and getting started with MPASSid.
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem "decidim-mpassid"
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ ```bash
37
+ $ bundle
38
+ ```
39
+
40
+ After installation, you can add the initializer running the following command:
41
+
42
+ ```bash
43
+ $ bundle exec rails generate decidim:mpassid:install
44
+ ```
45
+
46
+ You need to set the following configuration options inside the initializer:
47
+
48
+ - `:sp_entity_id` - The service provider entity ID, i.e. your applications
49
+ entity ID used to identify the service at the MPASSid SAML identity provider.
50
+ * Default: depends on the application's URL, e.g.
51
+ `https://www.example.org/users/auth/mpassid/metadata`
52
+ - `:auto_email_domain` - Defines the auto-email domain for automatically
53
+ verified email addresses for the identified users. This makes it easier for
54
+ the users to use the system as they don't have to go through any extra steps
55
+ verifying their email addresses, as they have already verified their identity.
56
+ * The auto-generated email format is similar to the following string:
57
+ `mpassid-756be91097ac490961fd04f121cb9550@example.org`. The email will
58
+ always have the `mpassid-` prefix and the domain part is defined by the
59
+ configuration option.
60
+
61
+ For more information about these options and possible other options, please
62
+ refer to the [`omniauth-mpassid`](https://github.com/mainio/omniauth-mpassid)
63
+ documentation.
64
+
65
+ The install generator will also enable the MPASSid authentication method for
66
+ OmniAuth by default by adding these lines your `config/secrets.yml`:
67
+
68
+ ```yml
69
+ default: &default
70
+ # ...
71
+ omniauth:
72
+ # ...
73
+ mpassid:
74
+ enabled: false
75
+ icon: account-login
76
+ development:
77
+ # ...
78
+ omniauth:
79
+ # ...
80
+ mpassid:
81
+ enabled: true
82
+ mode: test
83
+ icon: account-login
84
+ ```
85
+
86
+ This will enable the MPASSid authentication for the development environment
87
+ only. In case you want to enable it for other environments as well, apply the
88
+ OmniAuth configuration keys accordingly to other environments as well.
89
+
90
+ The development environment is hooking into the MPASSid testing endpoints by
91
+ default which is defined by the `mode: test` option in the OmniAuth
92
+ configuration. For environments that you want to hook into the MPASSid
93
+ production environment, you can omit this configuration option completely.
94
+
95
+ The example configuration will set the `account-login` icon for the the
96
+ authentication button from the Decidim's own iconset. In case you want to have a
97
+ better and more formal styling for the sign in button, you will need to
98
+ customize the sign in / sign up views.
99
+
100
+ ## Usage
101
+
102
+ After the installation steps, you will need to enable the MPASSid authorization
103
+ from Decidim's system management panel. After enabled, you can start using it.
104
+
105
+ This gem also provides a MPASSid sign in method which will automatically
106
+ authorize the user accounts. In case the users already have an account, they
107
+ can still authorize themselves using the MPASSid authorization.
108
+
109
+ ## Customization
110
+
111
+ For some specific needs, you may need to store extra metadata for the MPASSid
112
+ authorization or add new authorization configuration options for the
113
+ authorization.
114
+
115
+ This can be achieved by applying the following configuration to the module
116
+ inside the initializer described above:
117
+
118
+ ```ruby
119
+ # config/initializers/mpassid.rb
120
+
121
+ Decidim::Mpassid.configure do |config|
122
+ # ... keep the default configuration as is ...
123
+ # Add this extra configuration:
124
+ config.workflow_configurator = lambda do |workflow|
125
+ # When expiration is set to 0 minutes, it will never expire.
126
+ workflow.expires_in = 0.minutes
127
+ workflow.action_authorizer = "CustomMpassidActionAuthorizer"
128
+ workflow.options do |options|
129
+ options.attribute :custom_option, type: :string, required: false
130
+ end
131
+ end
132
+ config.metadata_collector_class = CustomMpassidMetadataCollector
133
+ end
134
+ ```
135
+
136
+ For the workflow configuration options, please refer to the
137
+ [decidim-verifications documentation](https://github.com/decidim/decidim/tree/master/decidim-verifications).
138
+
139
+ For the custom metadata collector, please extend the default class as follows:
140
+
141
+ ```ruby
142
+ # frozen_string_literal: true
143
+
144
+ class CustomMpassidMetadataCollector < Decidim::Mpassid::Verification::MetadataCollector
145
+ def metadata
146
+ super.tap do |data|
147
+ # You can access the SAML attributes using the `saml_attributes` accessor:
148
+ school_codes = saml_attributes[:school_code]
149
+ unless school_codes.blank?
150
+ extra = school_codes.map do |school_code|
151
+ "Extra data for: #{school_code}"
152
+ end
153
+
154
+ # This will actually add the data to the user's authorization metadata
155
+ # hash.
156
+ data[:extra] = extra.join(",")
157
+ end
158
+ end
159
+ end
160
+ end
161
+ ```
162
+
163
+ ## Contributing
164
+
165
+ See [Decidim](https://github.com/decidim/decidim).
166
+
167
+ ### Testing
168
+
169
+ To run the tests run the following in the gem development path:
170
+
171
+ ```bash
172
+ $ bundle
173
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rake test_app
174
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rspec
175
+ ```
176
+
177
+ Note that the database user has to have rights to create and drop a database in
178
+ order to create the dummy test app database.
179
+
180
+ In case you are using [rbenv](https://github.com/rbenv/rbenv) and have the
181
+ [rbenv-vars](https://github.com/rbenv/rbenv-vars) plugin installed for it, you
182
+ can add these environment variables to the root directory of the project in a
183
+ file named `.rbenv-vars`. In this case, you can omit defining these in the
184
+ commands shown above.
185
+
186
+ ### Test code coverage
187
+
188
+ If you want to generate the code coverage report for the tests, you can use
189
+ the `SIMPLECOV=1` environment variable in the rspec command as follows:
190
+
191
+ ```bash
192
+ $ SIMPLECOV=1 bundle exec rspec
193
+ ```
194
+
195
+ This will generate a folder named `coverage` in the project root which contains
196
+ the code coverage report.
197
+
198
+ ### Localization
199
+
200
+ If you would like to see this module in your own language, you can help with its
201
+ translation at Crowdin:
202
+
203
+ https://crowdin.com/project/decidim-mpassid
204
+
205
+ ## License
206
+
207
+ 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:mpassid: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:mpassid:install")
16
+ end
17
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Mpassid
5
+ class OmniauthCallbacksController < ::Decidim::Devise::OmniauthRegistrationsController
6
+ # Make the view helpers available needed in the views
7
+ helper Decidim::Mpassid::Engine.routes.url_helpers
8
+ helper_method :omniauth_registrations_path
9
+
10
+ skip_before_action :verify_authenticity_token, only: [:mpassid, :failure]
11
+ skip_after_action :verify_same_origin_request, only: [:mpassid, :failure]
12
+
13
+ # This is called always after the user returns from the authentication
14
+ # flow from the MPASSid identity provider.
15
+ def mpassid
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
+ # MPASSid 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.mpassid.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 MPASSid 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: "mpassid_nids",
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: "mpassid_nids",
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.mpassid.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
+ def metadata_collector
177
+ @metadata_collector ||= Decidim::Mpassid::Verification::Manager.metadata_collector_for(
178
+ saml_attributes
179
+ )
180
+ end
181
+
182
+ # Needs to be specifically defined because the core engine routes are not
183
+ # all properly loaded for the view and this helper method is needed for
184
+ # defining the omniauth registration form's submit path.
185
+ def omniauth_registrations_path(resource)
186
+ Decidim::Core::Engine.routes.url_helpers.omniauth_registrations_path(resource)
187
+ end
188
+
189
+ # Private: Create form params from omniauth hash
190
+ # Since we are using trusted omniauth data we are generating a valid signature.
191
+ def user_params_from_oauth_hash
192
+ return nil if oauth_data.empty?
193
+ return nil if saml_attributes.empty?
194
+ return nil if user_identifier.blank?
195
+
196
+ {
197
+ provider: oauth_data[:provider],
198
+ uid: user_identifier,
199
+ name: oauth_data[:info][:name],
200
+ # The nickname is automatically "parametrized" by Decidim core from
201
+ # the name string, i.e. it will be in correct format.
202
+ nickname: oauth_data[:info][:name],
203
+ oauth_signature: user_signature,
204
+ avatar_url: oauth_data[:info][:image],
205
+ raw_data: oauth_hash
206
+ }
207
+ end
208
+
209
+ def user_signature
210
+ @user_signature ||= OmniauthRegistrationForm.create_signature(
211
+ oauth_data[:provider],
212
+ user_identifier
213
+ )
214
+ end
215
+
216
+ # The MPASSid's assigned UID for the person. Note that this may change in
217
+ # case the user moves to another "registry". Not sure what they mean with
218
+ # "registry" in this context, but could be e.g. to another school or
219
+ # municipality.
220
+ def user_identifier
221
+ @user_identifier ||= oauth_data[:uid]
222
+ end
223
+
224
+ # Digested format of the person's identifier to be used in the
225
+ # auto-generated emails. This is used so that the actual identifier is not
226
+ # revealed directly to the end user.
227
+ def person_identifier_digest
228
+ @person_identifier_digest ||= Digest::MD5.hexdigest(
229
+ "MPASSID:#{user_identifier}:#{Rails.application.secrets.secret_key_base}"
230
+ )
231
+ end
232
+
233
+ def verified_email
234
+ @verified_email ||= begin
235
+ if Decidim::Mpassid.auto_email_domain
236
+ domain = Decidim::Mpassid.auto_email_domain
237
+ "mpassid-#{person_identifier_digest}@#{domain}"
238
+ end
239
+ end
240
+ end
241
+
242
+ def saml_attributes
243
+ @saml_attributes ||= oauth_hash[:extra][:saml_attributes]
244
+ end
245
+ end
246
+ end
247
+ end