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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +58 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/MIT-LICENSE +20 -0
- data/README.md +227 -0
- data/Rakefile +33 -0
- data/app/assets/config/ndr_authenticate_manifest.js +2 -0
- data/app/assets/images/ndr_authenticate/.keep +0 -0
- data/app/assets/javascripts/ndr_authenticate/ndr_authenticate.js +14 -0
- data/app/assets/stylesheets/ndr_authenticate/ndr_authenticate.scss +1 -0
- data/app/controllers/concerns/ndr_authenticate/authenticatable.rb +21 -0
- data/app/controllers/concerns/ndr_authenticate/devise_helpers.rb +17 -0
- data/app/controllers/concerns/ndr_authenticate/turbolinks.rb +31 -0
- data/app/controllers/concerns/ndr_authenticate/yubikey/authenticatable.rb +94 -0
- data/app/controllers/concerns/ndr_authenticate/yubikey/protectable.rb +103 -0
- data/app/controllers/ndr_authenticate/application_controller.rb +22 -0
- data/app/controllers/ndr_authenticate/authentication_controller.rb +27 -0
- data/app/controllers/ndr_authenticate/saml_sessions_controller.rb +46 -0
- data/app/controllers/ndr_authenticate/sessions_controller.rb +22 -0
- data/app/helpers/ndr_authenticate/application_helper.rb +22 -0
- data/app/helpers/ndr_authenticate/authentication_helper.rb +4 -0
- data/app/jobs/ndr_authenticate/application_job.rb +4 -0
- data/app/mailers/ndr_authenticate/application_mailer.rb +6 -0
- data/app/models/ndr_authenticate/application_record.rb +5 -0
- data/app/views/devise/passwords/edit.html.erb +23 -0
- data/app/views/devise/passwords/new.html.erb +18 -0
- data/app/views/devise/sessions/new.html.erb +21 -0
- data/app/views/devise/shared/_error_messages.html.erb +15 -0
- data/app/views/devise/shared/_links.html.erb +25 -0
- data/app/views/layouts/ndr_authenticate/ndr_authenticate.html.erb +47 -0
- data/app/views/ndr_authenticate/authentication/check_active.html.erb +30 -0
- data/app/views/ndr_authenticate/shared/_legal_notice.html.erb +13 -0
- data/app/views/ndr_authenticate/yubikey/protectable/_form.html.erb +44 -0
- data/app/views/ndr_authenticate/yubikey/protectable/_modal.html.erb +10 -0
- data/app/views/ndr_authenticate/yubikey/protectable/_panel.html.erb +13 -0
- data/app/views/ndr_authenticate/yubikey/protectable/challenge.html.erb +9 -0
- data/app/views/ndr_authenticate/yubikey/protectable/challenge.js.erb +41 -0
- data/app/views/shared/_flash_messages.html.erb +17 -0
- data/config/certificates/saml/certificate.pem +23 -0
- data/config/certificates/saml/encryption.phe.adfs.pem +18 -0
- data/config/certificates/saml/signing.phe.adfs.pem +19 -0
- data/config/initializers/devise.rb +37 -0
- data/config/locales/en.yml +15 -0
- data/config/routes.rb +24 -0
- data/lib/generators/ndr_authenticate/install/USAGE +10 -0
- data/lib/generators/ndr_authenticate/install/install_generator.rb +20 -0
- data/lib/generators/ndr_authenticate/install/templates/attribute-map.yml +77 -0
- data/lib/generators/ndr_authenticate/install/templates/migration.rb +13 -0
- data/lib/generators/ndr_authenticate/install/templates/ndr_authenticate.rb +31 -0
- data/lib/ndr_authenticate/connector.rb +45 -0
- data/lib/ndr_authenticate/engine.rb +76 -0
- data/lib/ndr_authenticate/saml_config.rb +39 -0
- data/lib/ndr_authenticate/version.rb +3 -0
- data/lib/ndr_authenticate/yubikey/verify.rb +26 -0
- data/lib/ndr_authenticate/yubikey.rb +7 -0
- data/lib/ndr_authenticate.rb +127 -0
- data/lib/tasks/ndr_authenticate_tasks.rake +4 -0
- 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
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
|
|
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
|