devise_duo_sec 0.0.7
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/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +34 -0
- data/app/assets/javascripts/devise_duo_security/Duo-Web-v2.js +366 -0
- data/app/assets/stylesheets/devise_duo_security/Duo-Frame.css +10 -0
- data/app/controllers/devise/duo_security_controller.rb +39 -0
- data/app/views/devise/duo_security/_test_iframe_response.html.erb +144 -0
- data/app/views/devise/duo_security/show.html.erb +15 -0
- data/lib/devise/duo_security/controllers/helpers.rb +41 -0
- data/lib/devise/duo_security/engine.rb +14 -0
- data/lib/devise/duo_security/version.rb +5 -0
- data/lib/devise_duo_sec.rb +43 -0
- data/lib/duo_web.rb +107 -0
- data/lib/tasks/devise_duo_security_tasks.rake +4 -0
- data/test/devise_duo_security_test.rb +16 -0
- data/test/dummy/Gemfile +10 -0
- data/test/dummy/Gemfile.lock +138 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/home_controller.rb +13 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/user.rb +6 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +42 -0
- data/test/dummy/config/environments/production.rb +78 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/devise.rb +259 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/devise.en.yml +60 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +7 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/migrate/20150320103707_devise_create_users.rb +42 -0
- data/test/dummy/db/schema.rb +34 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/test/fixtures/users.yml +11 -0
- data/test/dummy/test/models/user_test.rb +7 -0
- data/test/integration/navigation_test.rb +25 -0
- data/test/support/helpers.rb +40 -0
- data/test/test_helper.rb +46 -0
- metadata +337 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'duo_web'
|
2
|
+
|
3
|
+
class Devise::DuoSecurityController < DeviseController
|
4
|
+
prepend_before_action :set_resource
|
5
|
+
prepend_before_action :authenticate_scope!, only: [:show]
|
6
|
+
skip_before_action :verify_authenticity_token
|
7
|
+
|
8
|
+
include Devise::Controllers::Helpers
|
9
|
+
include Duo
|
10
|
+
|
11
|
+
def show
|
12
|
+
@host = DuoSecurity.configuration.host
|
13
|
+
@signature = Duo.sign_request(DuoSecurity.configuration.ikey, DuoSecurity.configuration.skey, DuoSecurity.configuration.app_secret, @resource.email)
|
14
|
+
end
|
15
|
+
|
16
|
+
def verify
|
17
|
+
authenticated_username = Duo.verify_response(DuoSecurity.configuration.ikey, DuoSecurity.configuration.skey, DuoSecurity.configuration.app_secret, params[:sig_response])
|
18
|
+
if authenticated_username
|
19
|
+
warden.session(resource_name)['duo_authenticated'] = true
|
20
|
+
redirect_to session["user_return_to"] || root_path
|
21
|
+
else
|
22
|
+
redirect_to send("#{resource_name}_duo_security_path")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def authenticate_scope!
|
29
|
+
# because we are a type of DeviseController authentication will not run again
|
30
|
+
# hence we need to set force => true to ensure a user is logged in!
|
31
|
+
send(:"authenticate_#{resource_name}!", :force => true)
|
32
|
+
self.resource = send("current_#{resource_name}")
|
33
|
+
@resource = resource
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_resource
|
37
|
+
@verify_path = send("verify_#{resource_name}_duo_security_path")
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
<html lang="en"><!--<![endif]--><head>
|
2
|
+
<meta charset="utf-8">
|
3
|
+
<title>Two-Factor Authentication</title>
|
4
|
+
<!--[if lt IE 9]>
|
5
|
+
<script src="/frame/static/js/lib/html5shiv.js?v=5a98a"></script>
|
6
|
+
<![endif]-->
|
7
|
+
<link rel="stylesheet" href="/frame/static/css/normalize.css?v=51888">
|
8
|
+
<link rel="stylesheet" href="/frame/static/fonts/opensans/opensans.css?v=f2b1a">
|
9
|
+
<link rel="stylesheet" href="/frame/static/fonts/ss-standard/ss-standard.css?v=56373">
|
10
|
+
<link rel="stylesheet" href="/frame/static/css/enroll_new/base.css?v=ca42a">
|
11
|
+
|
12
|
+
<link rel="stylesheet" href="/frame/static/css/enroll_new/prompt.css?v=14a67">
|
13
|
+
<link rel="stylesheet" href="/frame/static/css/tipsy.css?v=d2369">
|
14
|
+
|
15
|
+
</head>
|
16
|
+
<body>
|
17
|
+
<div class="base-wrapper">
|
18
|
+
<div class="base-header">
|
19
|
+
<h1><span class="duo">Duo</span> Two-Factor Authentication</h1>
|
20
|
+
|
21
|
+
<a class="help-link" href="http://guide.duosecurity.com/prompt" target="_blank" title="Need help?" tabindex="-1">
|
22
|
+
<i class="ss-help"></i>
|
23
|
+
</a>
|
24
|
+
|
25
|
+
</div>
|
26
|
+
<div class="base-main">
|
27
|
+
|
28
|
+
<div class="base-body">
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
<div class="status hidden">
|
35
|
+
|
36
|
+
</div>
|
37
|
+
<form action="/frame/prompt/new" method="post" id="login-form">
|
38
|
+
<input type="hidden" name="sid" value="NTljZjRjMDUyOTc4NDRmZjgxNjU2NzkzOGJiOWIzZGU=|95.131.110.106|1426862601|ac35e2db262e05c369d15dd8a1b422cc2ac6609d">
|
39
|
+
<label class="device-selector">
|
40
|
+
<b>Device:</b>
|
41
|
+
<select name="device">
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
<option value="phone1">
|
47
|
+
Android (+XX XXXX XX5265)
|
48
|
+
</option>
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
</select>
|
62
|
+
</label>
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
<fieldset data-device-index="phone1" class="" style="display: block;">
|
72
|
+
|
73
|
+
|
74
|
+
<label>
|
75
|
+
<input type="radio" name="factor" value="phone" checked="">
|
76
|
+
<b>Phone call</b>
|
77
|
+
<span class="helptext ss-help" title="We'll call your phone. Answer the call and press
|
78
|
+
|
79
|
+
any key
|
80
|
+
|
81
|
+
on your phone's keypad to authenticate."></span>
|
82
|
+
</label>
|
83
|
+
|
84
|
+
|
85
|
+
<div class="label passcode-label">
|
86
|
+
<input type="radio" name="factor" value="passcode">
|
87
|
+
<b>Passcode</b>
|
88
|
+
<input type="text" name="passcode">
|
89
|
+
|
90
|
+
<span class="helptext ss-help" title="Enter a passcode
|
91
|
+
|
92
|
+
|
93
|
+
sent via SMS or
|
94
|
+
|
95
|
+
|
96
|
+
provided by an administrator."></span>
|
97
|
+
</div>
|
98
|
+
|
99
|
+
<div class="sms-passcodes">
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
<p class="sms-provisioned">
|
105
|
+
Next SMS passcode starts with
|
106
|
+
<b class="next-passcode">2</b>
|
107
|
+
(<a href="#" class="need-codes sms-send-more" data-device-index="phone1">send more</a>)
|
108
|
+
</p>
|
109
|
+
<p class="sms-unprovisioned hidden">
|
110
|
+
<a href="#" class="need-codes" data-device-index="phone1">Send SMS passcodes</a>
|
111
|
+
</p>
|
112
|
+
</div>
|
113
|
+
|
114
|
+
</fieldset>
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
</form>
|
119
|
+
|
120
|
+
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
<div class="base-footer">
|
124
|
+
|
125
|
+
|
126
|
+
<button data-form="login-form" id="login-button" class="button button-green ss-navigateright right" type="submit">Log in</button>
|
127
|
+
|
128
|
+
</div>
|
129
|
+
|
130
|
+
</div>
|
131
|
+
<script src="/frame/static/shared/lib/jquery/jquery-legacy.min.js?v=65126"></script>
|
132
|
+
<script src="/frame/static/js/lib/jquery-postmessage.min.js?v=15c30"></script>
|
133
|
+
<!--[if lt IE 10]>
|
134
|
+
<script src="/frame/static/js/page/enroll_new/quirks.js?v=cb16b"></script>
|
135
|
+
<!--<![endif]-->
|
136
|
+
<script src="/frame/static/js/page/enroll_new/frame.js?v=bd543"></script>
|
137
|
+
|
138
|
+
|
139
|
+
<script src="/frame/static/js/lib/jquery.tipsy.js?v=cb2d5"></script>
|
140
|
+
<script src="/frame/static/js/page/enroll_new/prompt.js?v=3365b"></script>
|
141
|
+
|
142
|
+
|
143
|
+
|
144
|
+
</body></html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<%= javascript_include_tag "devise_duo_security/Duo-Web-v2" %>
|
2
|
+
<%= stylesheet_link_tag "devise_duo_security/Duo-Frame" %>
|
3
|
+
|
4
|
+
<script type="text/javascript" nonce="<%= @content_security_policy_nonce %>">
|
5
|
+
</script>
|
6
|
+
|
7
|
+
<iframe id="duo_iframe"
|
8
|
+
width="620"
|
9
|
+
height="330"
|
10
|
+
frameborder="0"
|
11
|
+
data-host="<%= @host %>"
|
12
|
+
data-sig-request="<%= @signature %>"
|
13
|
+
data-post-action="<%= @verify_path %>"
|
14
|
+
>
|
15
|
+
</iframe>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Devise::DuoSecurity
|
2
|
+
module Controllers
|
3
|
+
module Helpers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_filter :handle_two_factor_authentication
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def handle_two_factor_authentication
|
13
|
+
unless devise_controller?
|
14
|
+
Devise.mappings.keys.flatten.each do |scope|
|
15
|
+
if signed_in?(scope)
|
16
|
+
if (warden.session(scope)['duo_authenticated'].nil? or !warden.session(scope)['duo_authenticated'])
|
17
|
+
handle_failed_second_factor(scope)
|
18
|
+
end
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_failed_second_factor(scope)
|
26
|
+
if request.format.present? and request.format.html?
|
27
|
+
session["#{scope}_return_to"] = "#{request.path}?#{request.query_string}" if request.get?
|
28
|
+
redirect_to duo_authentication_path_for(scope)
|
29
|
+
else
|
30
|
+
render nothing: true, status: :unauthorized
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def duo_authentication_path_for(resource_or_scope = nil)
|
35
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
36
|
+
change_path = "#{scope}_duo_security_path"
|
37
|
+
send(change_path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'jquery-rails'
|
2
|
+
|
3
|
+
module Devise::DuoSecurity
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
ActiveSupport.on_load(:action_controller) do
|
6
|
+
include Devise::DuoSecurity::Controllers::Helpers
|
7
|
+
end
|
8
|
+
|
9
|
+
initializer "devise_duo_security.assets.precompile" do |app|
|
10
|
+
app.config.assets.precompile += %w(devise_duo_security/Duo-Web-v2.js)
|
11
|
+
app.config.assets.precompile += %w(devise_duo_security/Duo-Frame.css)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'devise'
|
2
|
+
require 'devise/duo_security/controllers/helpers'
|
3
|
+
require 'devise/duo_security/engine'
|
4
|
+
require 'duo_web'
|
5
|
+
|
6
|
+
module Devise
|
7
|
+
module DuoSecurity
|
8
|
+
class Configuration
|
9
|
+
attr_accessor :app_secret, :ikey, :skey, :host
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
yield(configuration)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: Isn't there a better way?
|
27
|
+
DuoSecurity = Devise::DuoSecurity
|
28
|
+
|
29
|
+
Devise.add_module :duo_security, :model => 'devise_duo_sec', :controller => :duo_security, :route => :duo_security
|
30
|
+
|
31
|
+
module ActionDispatch::Routing
|
32
|
+
class Mapper
|
33
|
+
protected
|
34
|
+
|
35
|
+
def devise_duo_security(mapping, controllers)
|
36
|
+
resource :duo_security, :only => [:show], :path => mapping.path_names[:duo_security], :controller => controllers[:duo_security] do
|
37
|
+
collection do
|
38
|
+
post :verify
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/duo_web.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
##
|
5
|
+
# A Ruby implementation of the Duo WebSDK
|
6
|
+
#
|
7
|
+
module Duo
|
8
|
+
DUO_PREFIX = 'TX'.freeze
|
9
|
+
APP_PREFIX = 'APP'.freeze
|
10
|
+
AUTH_PREFIX = 'AUTH'.freeze
|
11
|
+
|
12
|
+
DUO_EXPIRE = 300
|
13
|
+
APP_EXPIRE = 3600
|
14
|
+
|
15
|
+
IKEY_LEN = 20
|
16
|
+
SKEY_LEN = 40
|
17
|
+
AKEY_LEN = 40
|
18
|
+
|
19
|
+
ERR_USER = 'ERR|The username passed to sign_request() is invalid.'.freeze
|
20
|
+
ERR_IKEY = 'ERR|The Duo integration key passed to sign_request() is invalid.'.freeze
|
21
|
+
ERR_SKEY = 'ERR|The Duo secret key passed to sign_request() is invalid.'.freeze
|
22
|
+
ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least #{Duo::AKEY_LEN} characters.".freeze
|
23
|
+
|
24
|
+
# Sign a Duo 2FA request
|
25
|
+
# @param ikey [String] The Duo IKEY
|
26
|
+
# @param skey [String] The Duo SKEY
|
27
|
+
# @param akey [String] The Duo AKEY
|
28
|
+
# @param username [String] Username to authenticate as
|
29
|
+
def sign_request(ikey, skey, akey, username)
|
30
|
+
return Duo::ERR_USER if !username || username.empty?
|
31
|
+
return Duo::ERR_USER if username.include? '|'
|
32
|
+
return Duo::ERR_IKEY if !ikey || ikey.to_s.length != Duo::IKEY_LEN
|
33
|
+
return Duo::ERR_SKEY if !skey || skey.to_s.length != Duo::SKEY_LEN
|
34
|
+
return Duo::ERR_AKEY if !akey || akey.to_s.length < Duo::AKEY_LEN
|
35
|
+
|
36
|
+
vals = [username, ikey]
|
37
|
+
|
38
|
+
duo_sig = sign_vals(skey, vals, Duo::DUO_PREFIX, Duo::DUO_EXPIRE)
|
39
|
+
app_sig = sign_vals(akey, vals, Duo::APP_PREFIX, Duo::APP_EXPIRE)
|
40
|
+
|
41
|
+
return [duo_sig, app_sig].join(':')
|
42
|
+
end
|
43
|
+
|
44
|
+
# Verify a Duo 2FA request
|
45
|
+
# @param ikey [String] The Duo IKEY
|
46
|
+
# @param skey [String] The Duo SKEY
|
47
|
+
# @param akey [String] The Duo AKEY
|
48
|
+
# @param sig_response [String] Response from Duo service
|
49
|
+
def verify_response(ikey, skey, akey, sig_response)
|
50
|
+
begin
|
51
|
+
auth_sig, app_sig = sig_response.to_s.split(':')
|
52
|
+
auth_user = parse_vals(skey, auth_sig, Duo::AUTH_PREFIX, ikey)
|
53
|
+
app_user = parse_vals(akey, app_sig, Duo::APP_PREFIX, ikey)
|
54
|
+
rescue
|
55
|
+
return nil
|
56
|
+
end
|
57
|
+
|
58
|
+
return nil if auth_user != app_user
|
59
|
+
|
60
|
+
return auth_user
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def hmac_sha1(key, data)
|
66
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), key, data.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
def sign_vals(key, vals, prefix, expire)
|
70
|
+
exp = Time.now.to_i + expire
|
71
|
+
|
72
|
+
val_list = vals + [exp]
|
73
|
+
val = val_list.join('|')
|
74
|
+
|
75
|
+
b64 = Base64.encode64(val).delete("\n")
|
76
|
+
cookie = prefix + '|' + b64
|
77
|
+
|
78
|
+
sig = hmac_sha1(key, cookie)
|
79
|
+
return [cookie, sig].join('|')
|
80
|
+
end
|
81
|
+
|
82
|
+
def parse_vals(key, val, prefix, ikey)
|
83
|
+
ts = Time.now.to_i
|
84
|
+
|
85
|
+
parts = val.to_s.split('|')
|
86
|
+
return nil if parts.length != 3
|
87
|
+
u_prefix, u_b64, u_sig = parts
|
88
|
+
|
89
|
+
sig = hmac_sha1(key, [u_prefix, u_b64].join('|'))
|
90
|
+
|
91
|
+
return nil if hmac_sha1(key, sig) != hmac_sha1(key, u_sig)
|
92
|
+
|
93
|
+
return nil if u_prefix != prefix
|
94
|
+
|
95
|
+
cookie_parts = Base64.decode64(u_b64).to_s.split('|')
|
96
|
+
return nil if cookie_parts.length != 3
|
97
|
+
user, u_ikey, exp = cookie_parts
|
98
|
+
|
99
|
+
return nil if u_ikey != ikey
|
100
|
+
|
101
|
+
return nil if ts >= exp.to_i
|
102
|
+
|
103
|
+
return user
|
104
|
+
end
|
105
|
+
|
106
|
+
extend self
|
107
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Devise::DuoSecurityTest < ActiveSupport::TestCase
|
4
|
+
test "should be able to set and get configuration" do
|
5
|
+
c = Devise::DuoSecurity.configuration
|
6
|
+
c.app_secret = "secret"
|
7
|
+
c.ikey = "ikey"
|
8
|
+
c.skey = "skey"
|
9
|
+
c.host = "host"
|
10
|
+
|
11
|
+
assert_equal "secret", c.app_secret
|
12
|
+
assert_equal "ikey", c.ikey
|
13
|
+
assert_equal "skey", c.skey
|
14
|
+
assert_equal "host", c.host
|
15
|
+
end
|
16
|
+
end
|