ndr_authenticate 0.3.5

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +58 -0
  3. data/CODE_OF_CONDUCT.md +13 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +227 -0
  6. data/Rakefile +33 -0
  7. data/app/assets/config/ndr_authenticate_manifest.js +2 -0
  8. data/app/assets/images/ndr_authenticate/.keep +0 -0
  9. data/app/assets/javascripts/ndr_authenticate/ndr_authenticate.js +14 -0
  10. data/app/assets/stylesheets/ndr_authenticate/ndr_authenticate.scss +1 -0
  11. data/app/controllers/concerns/ndr_authenticate/authenticatable.rb +21 -0
  12. data/app/controllers/concerns/ndr_authenticate/devise_helpers.rb +17 -0
  13. data/app/controllers/concerns/ndr_authenticate/turbolinks.rb +31 -0
  14. data/app/controllers/concerns/ndr_authenticate/yubikey/authenticatable.rb +94 -0
  15. data/app/controllers/concerns/ndr_authenticate/yubikey/protectable.rb +103 -0
  16. data/app/controllers/ndr_authenticate/application_controller.rb +22 -0
  17. data/app/controllers/ndr_authenticate/authentication_controller.rb +27 -0
  18. data/app/controllers/ndr_authenticate/saml_sessions_controller.rb +46 -0
  19. data/app/controllers/ndr_authenticate/sessions_controller.rb +22 -0
  20. data/app/helpers/ndr_authenticate/application_helper.rb +22 -0
  21. data/app/helpers/ndr_authenticate/authentication_helper.rb +4 -0
  22. data/app/jobs/ndr_authenticate/application_job.rb +4 -0
  23. data/app/mailers/ndr_authenticate/application_mailer.rb +6 -0
  24. data/app/models/ndr_authenticate/application_record.rb +5 -0
  25. data/app/views/devise/passwords/edit.html.erb +23 -0
  26. data/app/views/devise/passwords/new.html.erb +18 -0
  27. data/app/views/devise/sessions/new.html.erb +21 -0
  28. data/app/views/devise/shared/_error_messages.html.erb +15 -0
  29. data/app/views/devise/shared/_links.html.erb +25 -0
  30. data/app/views/layouts/ndr_authenticate/ndr_authenticate.html.erb +47 -0
  31. data/app/views/ndr_authenticate/authentication/check_active.html.erb +30 -0
  32. data/app/views/ndr_authenticate/shared/_legal_notice.html.erb +13 -0
  33. data/app/views/ndr_authenticate/yubikey/protectable/_form.html.erb +44 -0
  34. data/app/views/ndr_authenticate/yubikey/protectable/_modal.html.erb +10 -0
  35. data/app/views/ndr_authenticate/yubikey/protectable/_panel.html.erb +13 -0
  36. data/app/views/ndr_authenticate/yubikey/protectable/challenge.html.erb +9 -0
  37. data/app/views/ndr_authenticate/yubikey/protectable/challenge.js.erb +41 -0
  38. data/app/views/shared/_flash_messages.html.erb +17 -0
  39. data/config/certificates/saml/certificate.pem +23 -0
  40. data/config/certificates/saml/encryption.phe.adfs.pem +18 -0
  41. data/config/certificates/saml/signing.phe.adfs.pem +19 -0
  42. data/config/initializers/devise.rb +37 -0
  43. data/config/locales/en.yml +15 -0
  44. data/config/routes.rb +24 -0
  45. data/lib/generators/ndr_authenticate/install/USAGE +10 -0
  46. data/lib/generators/ndr_authenticate/install/install_generator.rb +20 -0
  47. data/lib/generators/ndr_authenticate/install/templates/attribute-map.yml +77 -0
  48. data/lib/generators/ndr_authenticate/install/templates/migration.rb +13 -0
  49. data/lib/generators/ndr_authenticate/install/templates/ndr_authenticate.rb +31 -0
  50. data/lib/ndr_authenticate/connector.rb +45 -0
  51. data/lib/ndr_authenticate/engine.rb +76 -0
  52. data/lib/ndr_authenticate/saml_config.rb +39 -0
  53. data/lib/ndr_authenticate/version.rb +3 -0
  54. data/lib/ndr_authenticate/yubikey/verify.rb +26 -0
  55. data/lib/ndr_authenticate/yubikey.rb +7 -0
  56. data/lib/ndr_authenticate.rb +127 -0
  57. data/lib/tasks/ndr_authenticate_tasks.rake +4 -0
  58. metadata +287 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6f8600ab0772b344fbc6cef5ec38cf7c49867eef46d0dfa041dea2e96ef3ab7
4
+ data.tar.gz: 87678c88bebb7656d589dab278d42bf1721bf0176db0995e086a80688ad32c5b
5
+ SHA512:
6
+ metadata.gz: 1f7a72ddcee9bf7e853589c39061d73dc8a9a17503931fca31c989e9de0dd9377098c04bbee67c500fe7e8a560e2ddd23bc786156a5ce079194750292393a780
7
+ data.tar.gz: f6caaedff7462abbd36c72f2c160fb32f6e2e56772588959667923b73e7176a8eaeb940b26f0713e1b2162743cb819293736f77f7b59396e9d02fd8b5d7aceae
data/CHANGELOG.md ADDED
@@ -0,0 +1,58 @@
1
+ ## [Unreleased]
2
+ * no unreleased changes *
3
+
4
+ ## 0.3.5 / 2025-11-07
5
+ ### Fixed
6
+ * Support `ndr_ui` bootstrap 5
7
+ * Support Ruby 3.4, Rails 8.0. Drop support for Rails 7.0, Ruby 3.1
8
+
9
+ ## 0.3.4 / 2024-11-19
10
+ ### Fixed
11
+ * Support Ruby 3.2 and 3.3, Rails 7.1, 7.2
12
+ * Drop support for Ruby 3.0, Rails 6.1
13
+ * Update test application to latest Rails defaults
14
+
15
+ ## 0.3.3 / 2022-12-07
16
+ ### Fixed
17
+ * Drop support for Ruby 2.6
18
+ * Support Ruby 3.1, Rails 7.0
19
+ * Replace Public Health England naming with NHS Digital
20
+
21
+ ## 0.3.2 / 2022-12-01
22
+ ### Fixed
23
+ * Support Ruby 3.0
24
+ * Resolve Rails engine autoload warnings
25
+
26
+ ## 0.3.1 / 2022-05-20
27
+ ### Fixed
28
+ * Added temporary fix for issues around certificates following YubiCloud service changes.
29
+ * Updated expired certificates
30
+
31
+ ## 0.3.0 / 2020-07-22
32
+ ### Fixed
33
+ * Allow SSO routes to be disabled
34
+
35
+ ## 0.2.3.1 / 2020-09-09
36
+ ### Fixed
37
+ * Updated expired certificates
38
+
39
+ ## 0.2.3 / 2020-03-10
40
+ ### Fixed
41
+ * Added temporary fix for issues around certificates following YubiCloud service changes.
42
+
43
+ ## 0.2.2 / 2019-12-02
44
+ ### Fixed
45
+ * Fixed issue with OTP field not clearing on bad challenge for AJAX requests.
46
+ * Fixed issue with Turbolinks enabled applications causing ActionDispatch::Cookies::CookieOverflow
47
+ errors in certain situations.
48
+
49
+ ## 0.2.1 / 2019-11-08
50
+ ### Fixed
51
+ * Fixed UJS driver compatibility issue with Yubikey protected action SJR.
52
+
53
+ ## 0.2.0 / 2019-10-25
54
+ ### Added
55
+ * SAML authentication
56
+ * Yubikey protected actions
57
+ * Yubikey 2FA
58
+ * Rails 6 support
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019-2022 NHS Digital
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # NdrAuthenticate
2
+ This is the Public Health England (PHE) National Disease Registers (NDR) Authenticate ruby gem.
3
+ It is a Rails engine that provides domain authentication and verification capabilities.
4
+
5
+ ## Installation
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'ndr_authenticate'
10
+ ```
11
+
12
+ And then execute:
13
+ ```bash
14
+ $ bundle
15
+ ```
16
+
17
+ Or install it yourself as:
18
+ ```bash
19
+ $ gem install ndr_authenticate
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ Run the installation generator:
25
+ ```bash
26
+ $ bin/rails g ndr_authenticate:install
27
+ ```
28
+
29
+ This will install an initializer for NdrAuthenticate and some configuration files used for
30
+ single sign on via SAML.
31
+
32
+ Mount the engine in the hosting application's `config/routes.rb` file:
33
+ ```ruby
34
+ mount NdrAuthenticate::Engine, at: '/auth'
35
+ ```
36
+
37
+ Set some basic configuration information:
38
+
39
+ | Configuration | Description | Type | Default |
40
+ | ------------- | ----------- | ---- | ------- |
41
+ | `user_class` | Class name of the user model within the hosting application. | String | `'User'` |
42
+ | `sso_enabled` | Determines SAML Authentication availability. | Callable object | `->(_request) { Rails.env.production? }` |
43
+ | `yubikey_api_id` | API credentials for Yubico Web Services. | String | `nil` |
44
+ | `yubikey_api_key` | API credentials for Yubico Web Services. | String | `nil` |
45
+ | `invalid_otp_reasons` | Strategies for testing the validity of YubiKey one-time passwords. | Hash | `{ ... }` (omitted for brevity) |
46
+
47
+ ## View Helpers
48
+
49
+ NdrAuthenticate provides view helper methods for generating login/logout links. To use these
50
+ include the following in the hosting application's `ApplicationController`
51
+ (or other suitable location):
52
+
53
+ ```rails
54
+ helper NdrAuthenticate::ApplicationHelper
55
+ ```
56
+
57
+ ## Devise
58
+
59
+ NdrAuthenticate uses Devise for authentication. Given the highly configurable nature of Devise
60
+ this gem leaves (most) configuration up to the hosting application. To configure Devise for the
61
+ hosting application run the Devise installer and tweak the resultant initializer file as required.
62
+ See https://github.com/plataformatec/devise/#getting-started for more.
63
+
64
+ NdrAuthenticate will configure some Devise settings relating to engine behaviour and SAML
65
+ configuration. The hosting application is free to alter SAML settings as required
66
+ but `parent_controller` and `router_name` should typically be left alone.
67
+
68
+ It is not required to configure Devise routes (via e.g. `devise_for :users`) within the hosting
69
+ application as Devise is mounted within NdrAuthenticate. Run `bin/rails routes` to see the
70
+ available/generated routes.
71
+
72
+ ## Single Sign On / SAML Authentication
73
+
74
+ NdrAuthenticate provides domain authentication/SSO functionality via SAML request/response cycle.
75
+
76
+ This requires a little bit of configuration to be done within the hosting application:
77
+ * Firstly, configure Identity Provider (IdP) and Service Provider (SP) information.
78
+
79
+ For convenience, NdrAuthenticate can apply a PHE ADFS specific configuration. If you have run
80
+ the install then this will have been done automatically. Otherwise, add the following to
81
+ your NdrAuthenticate initializer:
82
+ ```ruby
83
+ NdrAuthenticate.configure do |config|
84
+ ...
85
+ config.load_defaults :phe
86
+ ```
87
+
88
+ NdrAuthenticate will configure SP information automatically. By default, the SP
89
+ `issuer`, `assertion_consumer_service_url` and `assertion_consumer_logout_service_url`
90
+ settings will be set to the respective `saml_metadata_url`, `saml_user_session_url` and
91
+ `idp_destroy_saml_user_session_url` values. Automatic IdP configuration is possible
92
+ by populating the `NdrAuthenticate#idp_metadata_url` setting and ensuring that the endpoint is
93
+ reachable from the app server.
94
+
95
+ Additional settings should be manually configured as required in an appropriate initializer:
96
+
97
+ ```ruby
98
+ Devise.saml_configure do |settings|
99
+ ...
100
+ end
101
+ ```
102
+
103
+ For further information on IdP/SP configuration see:
104
+ * https://github.com/apokalipto/devise_saml_authenticatable
105
+ * https://github.com/onelogin/ruby-saml
106
+
107
+
108
+ * Define how the attributes received from the IdP map to the attributes of the SP user
109
+ model in `config/attribute-map.yml`.
110
+
111
+ The IdP metadata should contain 0..n `<Attribute>` elements nested under the `<IDPSSODescriptor`
112
+ element, each of which should have a `Name` attribute/property; use the value of this when
113
+ mapping attributes between IdP and SP. Also note that every attribute provided by the IdP does
114
+ not need to be mapped, only those that are to be stored on the user model within the SP.
115
+
116
+ For example, an IdP providing the following attributes:
117
+
118
+ ```xml
119
+ <Attribute xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="E-Mail Address"/>
120
+ <Attribute xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Given Name"/>
121
+ <Attribute xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Surname"/>
122
+ <Attribute xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Name"/>
123
+ ```
124
+
125
+ ...and an SP with the following user resource:
126
+
127
+ ```ruby
128
+ class User < ApplicationRecord
129
+ attribute :email
130
+ attribute :first_name
131
+ attribute :last_name
132
+
133
+ ...
134
+ end
135
+ ```
136
+
137
+ ...may have an `attribute-map.yml` file like this:
138
+
139
+ ```yaml
140
+ ...
141
+ production:
142
+ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": email
143
+ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname": first_name
144
+ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname": last_name
145
+ ```
146
+
147
+ * Finally, add `:saml_authenticatable` to the `devise` call within the SP user model, e.g.
148
+
149
+ ```ruby
150
+ class User < ApplicationRecord
151
+ devise :saml_authenticatable,
152
+ :trackable,
153
+ ...
154
+ end
155
+ ```
156
+
157
+ ## Database Authentication
158
+ NdrAuthenticate has been set up such that the `:saml_authenticatable` and
159
+ `:database_authenticatable` Devise/Warden strategies should not be mutually exclusive. This should
160
+ allow hosting applications to use SAML SSO as a progressive enhancement or local database accounts
161
+ as a fallback (particularly useful in development environments).
162
+
163
+ ## Two Factor Authentication (2FA)
164
+ NdrAuthenticate uses Yubico YubiKeys as a second factor in authentication mechanisms.
165
+
166
+ #### 2FA Protected Actions
167
+ _To use this functionality please ensure that `yubikey_api_id` and `yubikey_api_key` values have
168
+ been configured with your Yubico Web Services credentials.
169
+ Visit <https://upgrade.yubico.com/getapikey/> to obtain an API key if you do not already have one._
170
+
171
+ Second factor authentication can be required for controller actions. To do this, call the
172
+ `yubikey_protected` macro in your controller class, passing in the actions that should require a
173
+ valid YubiKey one-time password to perform:
174
+
175
+ ```ruby
176
+ class PostsController < ApplicationController
177
+ yubikey_protected :create, :update, :destroy
178
+
179
+ ...
180
+ end
181
+ ```
182
+
183
+ By default, only basic validity checks are performed. These include guards against:
184
+ * blank values
185
+ * invalid format/structure
186
+ * replayed OTPWs
187
+
188
+ Host applications are encouraged to include additional challenges as required and production ready systems should almost certainly implement a check of OTPW/user combination (i.e. could this OTPW
189
+ have been generated from the submitting user's YubiKey?).
190
+
191
+ Challenges can be configured via the `invalid_otp_reasons` setting and should be callable objects
192
+ that accept `otp`, `user` and `context` parameters and return boolean values.
193
+
194
+ YubiKey protection is bypassed in the Rails development environment; add the debug flag to enable:
195
+
196
+ ```ruby
197
+ class PostsController < ApplicationController
198
+ yubikey_protected :create, :update, :destroy, debug: true
199
+
200
+ ...
201
+ end
202
+ ```
203
+
204
+ ## Contributing
205
+
206
+ 1. Fork it ( https://github.com/PublicHealthEngland/ndr_authenticate/fork )
207
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
208
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
209
+ 4. Push to the branch (`git push origin my-new-feature`)
210
+ 5. Create a new Pull Request
211
+
212
+ ### RuboCop
213
+ This project is configured to use RuboCop. Please ensure any contributions meet style guides,
214
+ wherever possible. You can run RuboCop with:
215
+
216
+ $ rubocop .
217
+
218
+ ### Coverage
219
+ Test coverage is measured by `simplecov` as part of the test suite. Its output can be viewed with:
220
+
221
+ $ open coverage/index.html
222
+
223
+ Please note that this project is released with a Contributor Code of Conduct. By participating
224
+ in this project you agree to abide by its terms.
225
+
226
+ ## License
227
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'NdrAuthenticate'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+ require 'ndr_dev_support/tasks'
26
+
27
+ Rake::TestTask.new(:test) do |t|
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/ndr_authenticate .js
2
+ //= link_directory ../stylesheets/ndr_authenticate .css
File without changes
@@ -0,0 +1,14 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
14
+ //= require ndr_ui
@@ -0,0 +1 @@
1
+ @import "ndr_ui/index";
@@ -0,0 +1,21 @@
1
+ module NdrAuthenticate
2
+ # Shared logic for controllers dealing with sessions.
3
+ module Authenticatable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ after_action :store_winning_strategy, only: :create
8
+ end
9
+
10
+ private
11
+
12
+ def sso_enabled?
13
+ NdrAuthenticate.sso_enabled&.call(request)
14
+ end
15
+
16
+ def store_winning_strategy
17
+ strategy = warden.winning_strategies[:user]
18
+ warden.session(:user)[:strategy] = strategy.class.name.demodulize.underscore.to_sym
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module NdrAuthenticate
2
+ # Additional Devise helper methods. Mixed into ActionController.
3
+ module DeviseHelpers
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper_method :user_authenticated_with?
8
+ end
9
+
10
+ def user_authenticated_with?(strategy)
11
+ return false unless user_signed_in?
12
+
13
+ winning_strategy = user_session.with_indifferent_access[:strategy]
14
+ winning_strategy&.to_sym == strategy.to_sym
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ module NdrAuthenticate
2
+ # Patch to prevent cookie overflow errors in applications running Turbolinks.
3
+ module Turbolinks
4
+ extend ActiveSupport::Concern
5
+
6
+ HEADER = 'Turbolinks-Referrer'.freeze
7
+
8
+ included do
9
+ around_action :skip_store_turbolinks_location_in_session
10
+ end
11
+
12
+ private
13
+
14
+ # Turbolinks will try to store referrer locations in the session when :redirect_to is called.
15
+ # This can be problematic if that location is particularly long, such as in the case of
16
+ # redirects to an IdP with a large SAML payload in the query string. We can temporarily
17
+ # manipulate request headers to bypass that session storage...
18
+ def skip_store_turbolinks_location_in_session
19
+ if request.headers.key?(HEADER)
20
+ turbolinks_referrer = request.headers[HEADER]
21
+ request.headers[HEADER] = nil
22
+
23
+ yield
24
+
25
+ request.headers[HEADER] = turbolinks_referrer
26
+ else
27
+ yield
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,94 @@
1
+ module NdrAuthenticate
2
+ module Yubikey
3
+ # Adds support for yubikey 2FA, using http_basic auth.
4
+ # Actual OTPW verification is done by an mod_authn_yubikey,
5
+ # in apache on an auth server. Those credentials are still
6
+ # passed through to the application as HTTP headers, which
7
+ # enables the yubikey to be tested against an application
8
+ # user.
9
+ #
10
+ # Some of this logic could be added to a custom warden strategy,
11
+ # but by modifying the controller we can (for example) pre-fill
12
+ # the username field if a yubikey is used.
13
+ module Authenticatable
14
+ extend ActiveSupport::Concern
15
+
16
+ # Expect yubikeys to be used in production. In the test environment,
17
+ # we enable the codepath, but are more lenient (see #should_check_current_yubikey?)
18
+ # - we can test yubikey behaviour using integration tests if we
19
+ # choose, but are not forced use them in functional tests.
20
+ PERFORM_2FA = Rails.env.test? || Rails.env.production?
21
+
22
+ included do
23
+ class_attribute :invalid_key_reasons, default: NdrAuthenticate.invalid_key_reasons
24
+
25
+ helper_method :current_yubikey
26
+ end
27
+
28
+ private
29
+
30
+ # If a user has got through the auth server with a yubikey
31
+ # that shouldn't be allowed to sign in (i.e. the keyfile is stale),
32
+ # fail any login attempts.
33
+ #
34
+ # The keyfile will sync regularly, but not instantly.
35
+ def check_current_yubikey!
36
+ return unless should_check_current_yubikey?
37
+
38
+ problem, _checker = invalid_key_reasons.detect do |_reason, checker|
39
+ checker.call(current_yubikey, current_user, self)
40
+ end
41
+
42
+ return unless problem
43
+
44
+ # Provide a hook for host application to do any custom logic (e.g. logging), if required.
45
+ failed_current_yubikey_check(problem)
46
+
47
+ sign_out(current_user)
48
+ throw(:warden, message: :second_factor_failure)
49
+ end
50
+
51
+ def failed_current_yubikey_check(problem); end
52
+
53
+ # If there is a signed-in user, in what circumstances should
54
+ # we verify they have submitted an appropriate yubikey too?
55
+ def should_check_current_yubikey?
56
+ # In tests, we generally don't submit the header every time; therefore,
57
+ # only test if it is explicitly given. This isn't ideal.
58
+ return if Rails.env.test? && request.authorization.nil?
59
+
60
+ # If 2FA auth is enabled, and we have a user with whom
61
+ # we can try and check the association of a yubikey with:
62
+ perform_2fa? && user_signed_in?
63
+ end
64
+
65
+ # Returns the yubikey in use, or nil.
66
+ def current_yubikey
67
+ public_id = http_basic_otpw.to_s[0, 12]
68
+ public_id.presence
69
+ end
70
+
71
+ def perform_2fa?
72
+ PERFORM_2FA
73
+ end
74
+
75
+ def http_basic_username
76
+ username = nil
77
+ authenticate_with_http_basic do |user, _p|
78
+ username = user
79
+ true
80
+ end
81
+ username
82
+ end
83
+
84
+ def http_basic_otpw
85
+ otpw = nil
86
+ authenticate_with_http_basic do |_u, password|
87
+ otpw = password
88
+ true
89
+ end
90
+ otpw
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,103 @@
1
+ module NdrAuthenticate
2
+ module Yubikey
3
+ # Adds functionality to require a second factor check before peforming specified actions.
4
+ module Protectable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :invalid_otp_reasons, default: NdrAuthenticate.invalid_otp_reasons
9
+ end
10
+
11
+ class_methods do
12
+ def yubikey_protected(*actions)
13
+ options = actions.extract_options!
14
+ debug = options.delete(:debug)
15
+
16
+ before_action :challenge_yubikey, only: actions, unless: lambda {
17
+ Rails.env.development? && !debug
18
+ }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # For general usage instructions see the README.
25
+ #
26
+ # Yubikey protected actions work by leveraging a `before_action`s ability to halt the
27
+ # request cycle via a `render` call. When a request arrives for a protected action we check
28
+ # the request params for a valid OTPW in the request. If one is not found then we issue a
29
+ # response and present the client with a form requesting a OTPW. This form will submit back
30
+ # to the originally requested endpoint and will contain a hidden field in which the original
31
+ # request's params are serialized/encoded for re-submission on the return trip. On receipt
32
+ # of a valid OTPW we restore the relayed params and allow the request to continue as normal.
33
+
34
+ def ndr_authenticate_params
35
+ params.fetch(:ndr_authenticate, {}).permit(:otp, :relay)
36
+ end
37
+
38
+ # NOTE: The custom header (`yubikey-required`) is a hacky fix to give the (default) JS
39
+ # response template a way of knowing if a challange was good/bad (thus providing a way to
40
+ # trigger modal lifecycle events). We could close the modal on sumbit but if the challenge
41
+ # is bad then we end up with modals popping in and out which is jarring.
42
+ def challenge_yubikey
43
+ if valid_otp?
44
+ restore_params
45
+ else
46
+ response.set_header('Cache-Control', 'no-store')
47
+ response.set_header('yubikey-required', 'required')
48
+
49
+ issue_challenge_to_user
50
+ end
51
+ end
52
+
53
+ def valid_otp?
54
+ return true if invalid_otp_reasons.empty?
55
+ return false unless ndr_authenticate_params.key?(:otp)
56
+
57
+ problem, _checker = invalid_otp_reasons.detect do |_reason, checker|
58
+ checker.call(ndr_authenticate_params[:otp], current_user, self)
59
+ end
60
+
61
+ return true unless problem
62
+
63
+ # Provide a hook for host application to do any custom logic (e.g. logging), if required.
64
+ failed_otp_challenge(problem)
65
+
66
+ false
67
+ end
68
+
69
+ def failed_otp_challenge(problem); end
70
+
71
+ def issue_challenge_to_user
72
+ # TODO: Add support for multipart forms. The current round-tripping approach would be
73
+ # costly in terms of network IO, not to mention the complexity of marshalling params.
74
+ raise 'Multipart forms are not currently supported' if multipart_form?
75
+
76
+ respond_to do |format|
77
+ format.any(:html, :js) do
78
+ template = 'ndr_authenticate/yubikey/protectable/challenge'
79
+ layout = 'ndr_authenticate/ndr_authenticate'
80
+
81
+ render template, layout: layout
82
+ end
83
+
84
+ format.any { head :forbidden }
85
+ end
86
+ end
87
+
88
+ def restore_params
89
+ return unless ndr_authenticate_params.key?(:relay)
90
+
91
+ relayed_params = JSON.parse(
92
+ Base64.urlsafe_decode64(ndr_authenticate_params.delete(:relay))
93
+ )
94
+ params.reverse_merge!(relayed_params)
95
+ end
96
+
97
+ def multipart_form?
98
+ content_type = request.headers['Content-Type']
99
+ content_type&.match?(/\Amultipart/)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,22 @@
1
+ module NdrAuthenticate
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ # Ensure Rails doesn't find any host layouts first:
6
+ layout 'ndr_authenticate/ndr_authenticate'
7
+
8
+ helper NdrUi::BootstrapHelper
9
+
10
+ private
11
+
12
+ # TODO: Make configurable (preferably in line with Devise documentation).
13
+ def after_sign_in_path_for(_resource)
14
+ main_app.root_path
15
+ end
16
+
17
+ # TODO: Make configurable (preferably in line with Devise documentation).
18
+ def after_sign_out_path_for(_resource)
19
+ main_app.root_path
20
+ end
21
+ end
22
+ end