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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
require 'ndr_authenticate/connector'
|
|
3
|
+
|
|
4
|
+
module NdrAuthenticate
|
|
5
|
+
class AuthenticationController < ApplicationController
|
|
6
|
+
# To check if user is active on AD
|
|
7
|
+
def check_active
|
|
8
|
+
email = params[:user_mail]
|
|
9
|
+
@status = ''
|
|
10
|
+
flash.clear
|
|
11
|
+
if email.present?
|
|
12
|
+
if email.match?(/\b[A-Z0-9._%a-z\-]+@phe\.gov\.uk\z/)
|
|
13
|
+
begin
|
|
14
|
+
conn = NdrAuthenticate::Connector.new
|
|
15
|
+
status = conn.search_for(params[:user_mail])
|
|
16
|
+
@status = status ? 'Active' : 'Inactive'
|
|
17
|
+
rescue => e
|
|
18
|
+
flash[:error] = e.message
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
flash[:error] = 'Please provide PHE email address only'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
render :check_active
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module NdrAuthenticate
|
|
2
|
+
# Manages SAML sessions.
|
|
3
|
+
class SamlSessionsController < Devise::SamlSessionsController
|
|
4
|
+
include Authenticatable
|
|
5
|
+
|
|
6
|
+
before_action :redirect_unless_sso_enabled, only: %i[new create]
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Capture NameID and SessionIndex values needed for the SLO request before terminating the
|
|
17
|
+
# local user session.
|
|
18
|
+
def destroy
|
|
19
|
+
@sessionindex = current_user.try(Devise.saml_session_index_key)
|
|
20
|
+
@nameid = current_user.try(Devise.saml_default_user_key)
|
|
21
|
+
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def redirect_unless_sso_enabled
|
|
28
|
+
redirect_to new_user_session_path unless sso_enabled?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Ensure that the SAML logout request contains the correct NameID and SessionIndex values.
|
|
32
|
+
# DeviseSamlAuthenticatable doesn't appear to do this by default.
|
|
33
|
+
def after_sign_out_path_for(*)
|
|
34
|
+
idp_entity_id = get_idp_entity_id(params)
|
|
35
|
+
settings = saml_config(idp_entity_id)
|
|
36
|
+
|
|
37
|
+
logout_request = OneLogin::RubySaml::Logoutrequest.new
|
|
38
|
+
logout_request.create(
|
|
39
|
+
settings.clone.tap do |request_settings|
|
|
40
|
+
request_settings.sessionindex = @sessionindex
|
|
41
|
+
request_settings.name_identifier_value = @nameid
|
|
42
|
+
end
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module NdrAuthenticate
|
|
2
|
+
# Handles user sessions.
|
|
3
|
+
class SessionsController < Devise::SessionsController
|
|
4
|
+
include Authenticatable
|
|
5
|
+
|
|
6
|
+
before_action :redirect_if_sso_enabled, only: %i[new create]
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def redirect_if_sso_enabled
|
|
19
|
+
redirect_to new_saml_user_session_path if sso_enabled?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module NdrAuthenticate
|
|
2
|
+
# NdrAuthenticate view helpers.
|
|
3
|
+
module ApplicationHelper
|
|
4
|
+
def sign_in_link(text = 'Sign In', **options)
|
|
5
|
+
return if user_signed_in?
|
|
6
|
+
|
|
7
|
+
link_to text, ndr_authenticate.new_user_session_path, options
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sign_out_link(text = 'Sign Out', **options)
|
|
11
|
+
return unless user_signed_in?
|
|
12
|
+
|
|
13
|
+
path = if user_authenticated_with?(:saml_authenticatable)
|
|
14
|
+
ndr_authenticate.destroy_saml_user_session_path
|
|
15
|
+
else
|
|
16
|
+
ndr_authenticate.destroy_user_session_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
link_to text, path, options.merge(method: :delete)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<h3>Reset your password</h3>
|
|
2
|
+
|
|
3
|
+
<div class="well">
|
|
4
|
+
<%= bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }, horizontal: 4) do |f| %>
|
|
5
|
+
<%= f.hidden_field :reset_password_token %>
|
|
6
|
+
|
|
7
|
+
<%= f.control_group :password, 'New password', {}, { class: 'col-md-4' } do %>
|
|
8
|
+
<%= f.password_field :password, autofocus: true, autocomplete: 'off', :'data-suggestible' => true %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<%= f.control_group :password_confirmation, 'Confirm new password', {}, { class: 'col-md-4' } do %>
|
|
12
|
+
<%= f.password_field :password_confirmation, autocomplete: 'off', :'data-suggestible' => true %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%= f.control_group nil, nil, {}, { class: 'col-md-4' } do %>
|
|
16
|
+
<%= f.submit 'Change my password', class: 'btn btn-primary btn-block' %>
|
|
17
|
+
|
|
18
|
+
<div style="text-align:center;margin-top:1em;">
|
|
19
|
+
<%= render "devise/shared/links" %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<h3>Forgot your password?</h3>
|
|
2
|
+
|
|
3
|
+
<div class="well">
|
|
4
|
+
<%= bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }, horizontal: 4) do |f| %>
|
|
5
|
+
|
|
6
|
+
<%= f.control_group :email, nil, {}, { class: 'col-md-4' } do %>
|
|
7
|
+
<%= f.text_field :email, autofocus: true %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%= f.control_group nil, nil, {}, { class: 'col-md-4' } do %>
|
|
11
|
+
<%= f.submit 'Send me reset password instructions', class: 'btn btn-primary btn-block' %>
|
|
12
|
+
|
|
13
|
+
<div style="text-align:center;margin-top:1em;">
|
|
14
|
+
<%= render "devise/shared/links" %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%= render 'ndr_authenticate/shared/legal_notice' %>
|
|
2
|
+
|
|
3
|
+
<div class="well">
|
|
4
|
+
<%= bootstrap_form_for(resource, as: resource_name, url: session_path(resource_name), horizontal: 4) do |f| %>
|
|
5
|
+
<%= f.control_group :email, nil, {}, { class: 'col-md-4' } do %>
|
|
6
|
+
<%= f.text_field :email %>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<%= f.control_group :password, nil, {}, { class: 'col-md-4' } do %>
|
|
10
|
+
<%= f.password_field :password, autocomplete: 'off' %>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<%= f.control_group nil, nil, {}, { class: 'col-md-4' } do %>
|
|
14
|
+
<%= f.submit 'Log in', class: 'btn btn-primary btn-block' %>
|
|
15
|
+
|
|
16
|
+
<div style="text-align:center;margin-top:1em;">
|
|
17
|
+
<%= render "devise/shared/links" %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% if resource.errors.any? %>
|
|
2
|
+
<div id="error_explanation">
|
|
3
|
+
<h2>
|
|
4
|
+
<%= I18n.t("errors.messages.not_saved",
|
|
5
|
+
count: resource.errors.count,
|
|
6
|
+
resource: resource.class.model_name.human.downcase)
|
|
7
|
+
%>
|
|
8
|
+
</h2>
|
|
9
|
+
<ul>
|
|
10
|
+
<% resource.errors.full_messages.each do |message| %>
|
|
11
|
+
<li><%= message %></li>
|
|
12
|
+
<% end %>
|
|
13
|
+
</ul>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%- if controller_name != 'sessions' %>
|
|
2
|
+
<%= link_to "Log in", new_session_path(resource_name) %><br />
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
|
6
|
+
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
|
10
|
+
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
|
14
|
+
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
|
18
|
+
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<%- if devise_mapping.omniauthable? %>
|
|
22
|
+
<%- resource_class.omniauth_providers.each do |provider| %>
|
|
23
|
+
<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>NdrAuthenticate</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= stylesheet_link_tag "ndr_authenticate/ndr_authenticate", media: "all" %>
|
|
9
|
+
<%= javascript_include_tag "ndr_authenticate/ndr_authenticate" %>
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bootstrap">
|
|
12
|
+
|
|
13
|
+
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
|
|
14
|
+
<div class="container-fluid">
|
|
15
|
+
<!-- Brand and toggle get grouped for better mobile display -->
|
|
16
|
+
<div class="navbar-header">
|
|
17
|
+
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
|
18
|
+
<span class="sr-only">Toggle navigation</span>
|
|
19
|
+
<span class="icon-bar"></span>
|
|
20
|
+
<span class="icon-bar"></span>
|
|
21
|
+
<span class="icon-bar"></span>
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
<%= link_to t('system.name', default: '« Back to App'.html_safe), '/', class: "navbar-brand" %>
|
|
25
|
+
</div>
|
|
26
|
+
<ul class="nav navbar-nav navbar-right">
|
|
27
|
+
<li><p class="navbar-text">NDR Authentication</p></li>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<div class="container">
|
|
34
|
+
<!-- An empty div -->
|
|
35
|
+
<div>
|
|
36
|
+
<p> </p>
|
|
37
|
+
<p> </p>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<%= render 'shared/flash_messages' %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<%= yield %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<h3>
|
|
2
|
+
Active directory user search
|
|
3
|
+
</h3>
|
|
4
|
+
|
|
5
|
+
<%= form_tag({ action: 'check_active' }, class: 'form form-horizontal') do %>
|
|
6
|
+
<div class="form-group">
|
|
7
|
+
<%= label_tag 'User PHE e-mail address', nil ,class: 'control-label col-md-3' %>
|
|
8
|
+
<div class="col-md-3">
|
|
9
|
+
<%= text_field_tag(:user_mail, params[:user_mail], class: 'form-control', placeholder: 'xxx@phe.gov.uk') %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<%= button_control_group do %>
|
|
13
|
+
<%= submit_tag 'Search', 'data-disable-with': 'Searching...' , class: 'btn btn-primary bob'%>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<% unless @status.empty? %>
|
|
18
|
+
<table class="table table-hover">
|
|
19
|
+
<tr>
|
|
20
|
+
<th colspan="2">
|
|
21
|
+
Status in Active Directory
|
|
22
|
+
<th>
|
|
23
|
+
</tr>
|
|
24
|
+
<tr>
|
|
25
|
+
<td class='info' colspan='2'>
|
|
26
|
+
<%= @status %>
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
<table>
|
|
30
|
+
<%end%>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= bootstrap_alert_tag(:info, dismissible: false) do %>
|
|
2
|
+
<strong>Legal Notice</strong>
|
|
3
|
+
This system is a restricted access system; only personnel authorised by
|
|
4
|
+
<%= t :organisation_abbreviation, scope: :system, default: 'PHE' %> may access this system.
|
|
5
|
+
Unauthorised access is prohibited and is contrary to the
|
|
6
|
+
<%= link_to 'Computer Misuse Act 1990', 'http://www.legislation.gov.uk/ukpga/1990/18/contents', :target => :new, :class => 'alert-link' %>,
|
|
7
|
+
which may result in criminal offences and a claim for damages.
|
|
8
|
+
All activity on this system is subject to monitoring.
|
|
9
|
+
If information collected reveals possible criminal activity or
|
|
10
|
+
activity that exceeds privileges, evidence of such activity may
|
|
11
|
+
be provided to the relevant authorities for further action.
|
|
12
|
+
<strong>By continuing past this point you expressly consent to this monitoring.</strong>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<%
|
|
2
|
+
url = url_for(controller: params[:controller], action: params[:action])
|
|
3
|
+
method = params[:_method] || request.method
|
|
4
|
+
relay = params.dig(:ndr_authenticate, :relay) || Base64.urlsafe_encode64(
|
|
5
|
+
params.except(:authenticity_token).to_json
|
|
6
|
+
)
|
|
7
|
+
i18n_scope = %i[ndr_authenticate yubikey protectable challenge]
|
|
8
|
+
%>
|
|
9
|
+
|
|
10
|
+
<% content_for :header do %>
|
|
11
|
+
<div class="text-center">
|
|
12
|
+
<h4>
|
|
13
|
+
<span class="glyphicon glyphicon-lock"></span>
|
|
14
|
+
<strong><%= t(:title, scope: i18n_scope) %></strong>
|
|
15
|
+
</h4>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% content_for :body do %>
|
|
20
|
+
<% if params.dig(:ndr_authenticate, :otp) %>
|
|
21
|
+
<%= bootstrap_alert_tag(:danger, t(:invalid_otp, scope: i18n_scope)) %>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<div class="text-center">
|
|
25
|
+
<p class="lead"><%= t(:lead, scope: i18n_scope) %></p>
|
|
26
|
+
<p><%= t(:instructions, scope: i18n_scope) %></p>
|
|
27
|
+
<br />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<%= bootstrap_form_with(url: url, scope: :ndr_authenticate, method: method, local: local) do |form| %>
|
|
31
|
+
<%= form.hidden_field(:relay, value: relay) %>
|
|
32
|
+
|
|
33
|
+
<%= form.control_group nil do %>
|
|
34
|
+
<%= form.password_field(:otp, autofocus: true) %>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<br />
|
|
38
|
+
<div class="text-center">
|
|
39
|
+
<%= form.submit 'Submit', class: 'btn btn-primary' %>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
44
|
+
<%= render(layout: layout) {} %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="modal" id="yubikey-challenge" tabindex="-1" role="dialog">
|
|
2
|
+
<div class="modal-dialog" role="document">
|
|
3
|
+
<div class="modal-content">
|
|
4
|
+
<%#= bootstrap_modal_dialog_tag id: 'yubikey-challenge' do %>
|
|
5
|
+
<%= bootstrap_modal_header_tag yield(:header), dismissible: true %>
|
|
6
|
+
<%= bootstrap_modal_body_tag yield(:body) %>
|
|
7
|
+
<%# end %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% if defined?(bootstrap_panel_tag) %><%# Old bootstrap 3 codepath %>
|
|
2
|
+
<%= bootstrap_panel_tag yield(:header), id: 'yubikey-challenge', style: 'margin-top: 50px;' do %>
|
|
3
|
+
<%= bootstrap_panel_body_tag do %>
|
|
4
|
+
<%= yield(:body) %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% else %><%# New bootstrap 5 codepath %>
|
|
8
|
+
<%= bootstrap_card_tag yield(:header), nil, id: 'yubikey-challenge', style: 'margin-top: 50px;' do %>
|
|
9
|
+
<%= bootstrap_card_body_tag do %>
|
|
10
|
+
<%= yield(:body) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# TODO: This could/should be packaged up as a Stimulus controller.
|
|
3
|
+
path = 'ndr_authenticate/yubikey/protectable'
|
|
4
|
+
form = render "#{path}/form", layout: "#{path}/modal", local: false
|
|
5
|
+
alert = bootstrap_alert_tag(:danger, t('ndr_authenticate.yubikey.protectable.challenge.invalid_otp'))
|
|
6
|
+
%>
|
|
7
|
+
|
|
8
|
+
var form = '<%= j(form) %>';
|
|
9
|
+
var challenge = '#yubikey-challenge';
|
|
10
|
+
var input = 'input[name="ndr_authenticate[otp]"';
|
|
11
|
+
|
|
12
|
+
// New challenge form; add it to the DOM and set up some event listeners...
|
|
13
|
+
if($(challenge).length == 0) {
|
|
14
|
+
$('body').append(form);
|
|
15
|
+
|
|
16
|
+
// Remove the challenge form once it's served it's purpose (or user backs out); we'll replace it
|
|
17
|
+
// wholesale the next time it's needed.
|
|
18
|
+
$(challenge).on('hidden.bs.modal', function() {
|
|
19
|
+
$(challenge).remove();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Seems like Rails UJS doesn't allow these events to bubble?
|
|
23
|
+
$('form', challenge).on('ajax:complete', function onChallenge(event) {
|
|
24
|
+
var xhr = (arguments.length == 1 ? event.detail[0] : arguments[1]);
|
|
25
|
+
|
|
26
|
+
// If the response has the `yubikey-required` header then a bad OTP must have been submitted
|
|
27
|
+
// and we should clear form and provide some user feedback...
|
|
28
|
+
if(xhr.getResponseHeader('yubikey-required')) {
|
|
29
|
+
if($('.modal-body .alert', challenge).length == 0)
|
|
30
|
+
$('.modal-body', challenge).prepend('<%= j(alert) %>');
|
|
31
|
+
|
|
32
|
+
$(input).val(null);
|
|
33
|
+
} else {
|
|
34
|
+
// Successful challenge; close (and trigger removal of) the modal form.
|
|
35
|
+
$(challenge).modal('hide');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
$(challenge).modal('show');
|
|
41
|
+
$(input).focus();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<% if flash[:notice].present? %>
|
|
3
|
+
<%= bootstrap_alert_tag(:info, safe_join(Array(flash[:notice]), tag(:br))) %>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% if flash[:error].present? %>
|
|
7
|
+
<%= bootstrap_alert_tag(:danger, safe_join(Array(flash[:error]), tag(:br))) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<% if flash[:alert].present? %>
|
|
11
|
+
<%= bootstrap_alert_tag(:danger, safe_join(Array(flash[:alert]), tag(:br))) %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% if flash[:warning].present? %>
|
|
15
|
+
<%= bootstrap_alert_tag(:warning, safe_join(Array(flash[:alert]), tag(:br))) %>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDvjCCAqYCCQCR3CjZjsYCszANBgkqhkiG9w0BAQsFADCBoDELMAkGA1UEBhMC
|
|
3
|
+
R0IxDjAMBgNVBAgMBUNhbWJzMRIwEAYDVQQHDAlDYW1icmlkZ2UxDDAKBgNVBAoM
|
|
4
|
+
A1BIRTEMMAoGA1UECwwDTkRSMSYwJAYDVQQDDB1ORFIgQXV0aGVudGljYXRlIFNB
|
|
5
|
+
TUwgU2lnbmluZzEpMCcGCSqGSIb3DQEJARYaYWRtaW5pc3RyYXRvckBlY3JpYy5u
|
|
6
|
+
aHMudWswHhcNMjAwOTA5MDkyNTEzWhcNMzAwOTA3MDkyNTEzWjCBoDELMAkGA1UE
|
|
7
|
+
BhMCR0IxDjAMBgNVBAgMBUNhbWJzMRIwEAYDVQQHDAlDYW1icmlkZ2UxDDAKBgNV
|
|
8
|
+
BAoMA1BIRTEMMAoGA1UECwwDTkRSMSYwJAYDVQQDDB1ORFIgQXV0aGVudGljYXRl
|
|
9
|
+
IFNBTUwgU2lnbmluZzEpMCcGCSqGSIb3DQEJARYaYWRtaW5pc3RyYXRvckBlY3Jp
|
|
10
|
+
Yy5uaHMudWswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXJyFcZER7
|
|
11
|
+
NjBJA93YYdZK3Ck4Yca9td1e++wysP7mpAIKC6wwkcmkkpKQgLvmxIwFhrswpRLm
|
|
12
|
+
HBhex0q3xE1GWOqHO3Ve8c6yki4PxbkLHWhHhaCrrvFAZt0mS5fReezNjAElfVZv
|
|
13
|
+
ZmreH/GqQ4HH7ZR66OkCK3EqlMHSfICDKpjrHswP4GjeXvxf859+r6OC0W+PEnfm
|
|
14
|
+
sxmRdpyX+XKuf13DZ3zZfEaEPwb1Kg74YtaE/d0WdeZGH/d42O4cWGlyCL6xAoE7
|
|
15
|
+
AOHi96ytTqvd6QDcQpUOwQKG4iytehHFtkPZTMD2HL6GUmce6mbQDWyR8/7sI1tg
|
|
16
|
+
Opn+B5/+WpndAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHv8y6VayX7AW473mhso
|
|
17
|
+
jFHh/PQxcj3iLXIT2xC96tfUiz93RQw1ekdSQqcdRD9sRPx6V+RB8rhCBr3TtUXz
|
|
18
|
+
iyVFXWWtIdVRR4I3iLaw1SQXKhYaWZvaD5fZVl38aqBPzXiF0wdPm138BBo1PjDL
|
|
19
|
+
u8JOh1ArbTsJk7wkmweRsl2NmoBhFGxpvRvN0QqlMsHJIKHw4R7k7i16ETQfLkJo
|
|
20
|
+
WYDj5kGylb8V2aQTf7peZy81VdfQnc2z+/9SvsHQjwDUmjZK8iqirrPgtbxQF/3h
|
|
21
|
+
vO7EkLIbEitDdkvBD5RecEqqpyCR8Z1oTGvQPM28644yMVop3MjSlvwWb06uxlcY
|
|
22
|
+
Vzk=
|
|
23
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIC3DCCAcSgAwIBAgIQKl6Cdeu33JVEhYdcQiJWxzANBgkqhkiG9w0BAQsFADAq
|
|
3
|
+
MSgwJgYDVQQDEx9BREZTIEVuY3J5cHRpb24gLSBmcy5waGUuZ292LnVrMB4XDTIw
|
|
4
|
+
MDYyMTIzMjA1NVoXDTIxMDYyMTIzMjA1NVowKjEoMCYGA1UEAxMfQURGUyBFbmNy
|
|
5
|
+
eXB0aW9uIC0gZnMucGhlLmdvdi51azCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
|
6
|
+
AQoCggEBAKumzkThSDUSzBMUPqM7hqk7nHKwZf9fsUBhYI+b7kKbjC6+bJFEIPBX
|
|
7
|
+
RKZPkDKb9oCCoFAogZIcxE/RLLAmK4ZC7sfFNd9SYp89bLAoL3sKFM9HEpMYFDKz
|
|
8
|
+
isFH83btch1vXygovL8XdzmIyv7C+jNwviU5bpUDeA+BlNBl0OFb7792HY+dBoGX
|
|
9
|
+
OsoUMsNRrk8n1dj1CnOJp3PvnYgv9YeeF2bN/R/KUn3ejdNdCCfAatpB+8kN2MHq
|
|
10
|
+
vRJXaeJowubf8IUy59iTEP+R4+8r65cjsizTqxyo75GRlLJxDHa8vOjluHvveR0x
|
|
11
|
+
EAbi1G6AFuI2vw6WXtlntca/toEZjSsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
|
|
12
|
+
d8BEGKqsg5OcMio3UJZo/CTJjtGBd6thCuAFPGrfno4Et206GqB7MzThzuvI10Vc
|
|
13
|
+
eEx/QLIP9WiVflg8Cq7tZWM8t10gRLW2VeZ/wIqtaoVeHGSRKOJwOPg0ENhHZLod
|
|
14
|
+
AQsIo659STd6BNypNT3p387aNS2vER/mqJC/OgDiqLtSGGcYJQRb6RmWymTo7GM0
|
|
15
|
+
NLwaW/CE3VReiifBKt9s26bs3tinhOMsd2f1r5iamJP0p/bp3dJ/aerkZDgWNaGp
|
|
16
|
+
QYoPT2ywkqa2kSqg3K7McFMbdtC8LdvyOECwbCKUeSIzaCFSx/KaUY7PWGtVDSnf
|
|
17
|
+
S8/WUIRuSH8QoDY5KxxoMw==
|
|
18
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIC1jCCAb6gAwIBAgIQJURsk+h0Fo5A+bf3mxtwNDANBgkqhkiG9w0BAQsFADAn
|
|
3
|
+
MSUwIwYDVQQDExxBREZTIFNpZ25pbmcgLSBmcy5waGUuZ292LnVrMB4XDTIwMDYy
|
|
4
|
+
MTIzMjA1N1oXDTIxMDYyMTIzMjA1N1owJzElMCMGA1UEAxMcQURGUyBTaWduaW5n
|
|
5
|
+
IC0gZnMucGhlLmdvdi51azCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
|
6
|
+
AL/DhLdxZobVXZQI858Fo1K1so1StuaygMbqIu3IaMHnpNEN3lXFaUFjJeo17k0B
|
|
7
|
+
V3A6E7A7O/h7Tu//sCUc6tN66+jAOXwB/yVgaMW/DFou01EJMWlBZqzBKkz5nSWJ
|
|
8
|
+
kNlR6f2XB/NoGKyULFbWAt5I8onkjCuawsDP/FameDasMMmpBPKoFqNrHmdcJGAw
|
|
9
|
+
K1JLxdo/Oc6HOImrWgexBiH6xV0KrXf4OE5JrqUC44H5BDM/bxnLQWca2plbVRtf
|
|
10
|
+
yDnqAJ2KmaXZjIYQScRMK1/UocqYmUvPIvCy9gdCQ++4kLbJUtrvxNg8RUEHD9oO
|
|
11
|
+
7RY7mMnmfTyWc78J1CD6lHUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAp1MngXjt
|
|
12
|
+
IRnSqVHoSnzRcQwZ+wxzP/Apgr0yjkno2/0JXDxZWkksIxU5UoBOdZDde0/vl+vS
|
|
13
|
+
VoZmUzv/SPtrdNNJE86TJ6cYbj+VfF6LLhyzr2OAzmgqa2qkDktvTMO5RyksPFXY
|
|
14
|
+
Vx89rkh39SHzi5i5ZNFeOLRBJlyP3rT+aWmX1WgHXdM/OCnqLO7v4NicTsV1woys
|
|
15
|
+
fBN95lmIxM6Mom5WuVNs/LSSASATcyXsfsMtAyNWFB58r+L9vp7QuMSQw3KPGLtX
|
|
16
|
+
Jo+1YldmQNZamd5GAM2NOHNFUKGk473qA8ZYr5wVZX/E5rBU8TNZcYVAob8vbOXJ
|
|
17
|
+
Dm+MNclHHtFmkA==
|
|
18
|
+
-----END CERTIFICATE-----
|
|
19
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Use this hook to configure devise mailer, warden hooks and so forth.
|
|
4
|
+
# Many of these configuration options can be set straight in your model.
|
|
5
|
+
Devise.setup do |config|
|
|
6
|
+
# ==> Controller configuration
|
|
7
|
+
# Configure the parent class to the devise controllers.
|
|
8
|
+
# config.parent_controller = 'DeviseController'
|
|
9
|
+
config.parent_controller = 'NdrAuthenticate::ApplicationController'
|
|
10
|
+
|
|
11
|
+
# ==> Mountable engine configurations
|
|
12
|
+
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
|
13
|
+
# is mountable, there are some extra configurations to be taken into account.
|
|
14
|
+
# The following options are available, assuming the engine is mounted as:
|
|
15
|
+
#
|
|
16
|
+
# mount MyEngine, at: '/my_engine'
|
|
17
|
+
#
|
|
18
|
+
# The router that invoked `devise_for`, in the example above, would be:
|
|
19
|
+
# config.router_name = :my_engine
|
|
20
|
+
config.router_name = :ndr_authenticate
|
|
21
|
+
|
|
22
|
+
# ==> Configuration for :saml_authenticatable
|
|
23
|
+
|
|
24
|
+
# Create user if the user does not exist. (Default is false)
|
|
25
|
+
config.saml_create_user = true
|
|
26
|
+
|
|
27
|
+
# Update the attributes of the user after a successful login. (Default is false)
|
|
28
|
+
config.saml_update_user = true
|
|
29
|
+
|
|
30
|
+
# Set the default user key. The user will be looked up by this key. Make
|
|
31
|
+
# sure that the Authentication Response includes the attribute.
|
|
32
|
+
config.saml_default_user_key = :email
|
|
33
|
+
|
|
34
|
+
# Optional. This stores the session index defined by the IDP during login. If provided it will
|
|
35
|
+
# be used as a salt for the user's session to facilitate an IDP initiated logout request.
|
|
36
|
+
config.saml_session_index_key = :session_index
|
|
37
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
en:
|
|
2
|
+
ndr_authenticate:
|
|
3
|
+
yubikey:
|
|
4
|
+
protectable:
|
|
5
|
+
challenge:
|
|
6
|
+
title: Authentication Required
|
|
7
|
+
lead: The action you are trying to perform requires YubiKey authentication.
|
|
8
|
+
instructions: >
|
|
9
|
+
Press your YubiKey’s gold contact to insert a one-time password into the field below
|
|
10
|
+
and then click "Submit" to continue.
|
|
11
|
+
invalid_otp: Invalid one-time password
|
|
12
|
+
devise:
|
|
13
|
+
failure:
|
|
14
|
+
user:
|
|
15
|
+
second_factor_failure: There is a problem with your yubikey.
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
NdrAuthenticate::Engine.routes.draw do
|
|
2
|
+
get 'check_active', to: 'authentication#check_active'
|
|
3
|
+
post 'check_active', to: 'authentication#check_active'
|
|
4
|
+
|
|
5
|
+
if NdrAuthenticate.sso_enabled_ever?
|
|
6
|
+
devise_for :users, class_name: NdrAuthenticate.user_class,
|
|
7
|
+
module: :devise,
|
|
8
|
+
skip: %i[sessions saml_authenticatable]
|
|
9
|
+
|
|
10
|
+
devise_scope :user do
|
|
11
|
+
get :sign_in, to: 'sessions#new', as: :new_user_session
|
|
12
|
+
post :sign_in, to: 'sessions#create', as: :user_session
|
|
13
|
+
delete :sign_out, to: 'sessions#destroy', as: :destroy_user_session
|
|
14
|
+
|
|
15
|
+
scope :saml, controller: :saml_sessions do
|
|
16
|
+
get :metadata, action: :metadata, as: :saml_metadata
|
|
17
|
+
get :sign_in, action: :new, as: :new_saml_user_session
|
|
18
|
+
post :auth, action: :create, as: :saml_user_session
|
|
19
|
+
delete :sign_out, action: :destroy, as: :destroy_saml_user_session
|
|
20
|
+
match '/idp_sign_out', action: :idp_sign_out, as: :idp_destroy_saml_user_session, via: %i[get post delete]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Creates boilerplate configuration for NdrAuthenticate.
|
|
3
|
+
|
|
4
|
+
Example:
|
|
5
|
+
rails generate ndr_authenticate:install
|
|
6
|
+
|
|
7
|
+
This will create:
|
|
8
|
+
config/initializers/ndr_authenticate.rb
|
|
9
|
+
config/initializers/saml_authenticatable.rb
|
|
10
|
+
config/attribute-map.yml
|