decidim-mpassid 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE-AGPLv3.txt +661 -0
- data/README.md +207 -0
- data/Rakefile +17 -0
- data/app/controllers/decidim/mpassid/omniauth_callbacks_controller.rb +247 -0
- data/app/controllers/decidim/mpassid/verification/authorizations_controller.rb +19 -0
- data/config/locales/en.yml +21 -0
- data/config/locales/fi.yml +20 -0
- data/config/locales/sv.yml +20 -0
- data/lib/decidim/mpassid.rb +105 -0
- data/lib/decidim/mpassid/engine.rb +101 -0
- data/lib/decidim/mpassid/mail_interceptors.rb +9 -0
- data/lib/decidim/mpassid/mail_interceptors/generated_recipients_interceptor.rb +25 -0
- data/lib/decidim/mpassid/test/cert_store.rb +21 -0
- data/lib/decidim/mpassid/test/runtime.rb +48 -0
- data/lib/decidim/mpassid/verification.rb +5 -0
- data/lib/decidim/mpassid/verification/engine.rb +43 -0
- data/lib/decidim/mpassid/verification/manager.rb +17 -0
- data/lib/decidim/mpassid/verification/metadata_collector.rb +58 -0
- data/lib/decidim/mpassid/version.rb +8 -0
- data/lib/generators/decidim/mpassid/install_generator.rb +127 -0
- data/lib/generators/templates/mpassid_initializer.rb +10 -0
- data/lib/generators/templates/mpassid_initializer_test.rb +3 -0
- metadata +107 -0
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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
|