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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/devise_duo_security/Duo-Web-v2.js +366 -0
  6. data/app/assets/stylesheets/devise_duo_security/Duo-Frame.css +10 -0
  7. data/app/controllers/devise/duo_security_controller.rb +39 -0
  8. data/app/views/devise/duo_security/_test_iframe_response.html.erb +144 -0
  9. data/app/views/devise/duo_security/show.html.erb +15 -0
  10. data/lib/devise/duo_security/controllers/helpers.rb +41 -0
  11. data/lib/devise/duo_security/engine.rb +14 -0
  12. data/lib/devise/duo_security/version.rb +5 -0
  13. data/lib/devise_duo_sec.rb +43 -0
  14. data/lib/duo_web.rb +107 -0
  15. data/lib/tasks/devise_duo_security_tasks.rake +4 -0
  16. data/test/devise_duo_security_test.rb +16 -0
  17. data/test/dummy/Gemfile +10 -0
  18. data/test/dummy/Gemfile.lock +138 -0
  19. data/test/dummy/README.rdoc +28 -0
  20. data/test/dummy/Rakefile +6 -0
  21. data/test/dummy/app/assets/javascripts/application.js +15 -0
  22. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  23. data/test/dummy/app/controllers/application_controller.rb +5 -0
  24. data/test/dummy/app/controllers/home_controller.rb +13 -0
  25. data/test/dummy/app/helpers/application_helper.rb +2 -0
  26. data/test/dummy/app/models/user.rb +6 -0
  27. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/test/dummy/bin/bundle +3 -0
  29. data/test/dummy/bin/rails +4 -0
  30. data/test/dummy/bin/rake +4 -0
  31. data/test/dummy/bin/setup +29 -0
  32. data/test/dummy/config.ru +4 -0
  33. data/test/dummy/config/application.rb +26 -0
  34. data/test/dummy/config/boot.rb +5 -0
  35. data/test/dummy/config/database.yml +25 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +42 -0
  38. data/test/dummy/config/environments/production.rb +78 -0
  39. data/test/dummy/config/environments/test.rb +42 -0
  40. data/test/dummy/config/initializers/assets.rb +11 -0
  41. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  42. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  43. data/test/dummy/config/initializers/devise.rb +259 -0
  44. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  45. data/test/dummy/config/initializers/inflections.rb +16 -0
  46. data/test/dummy/config/initializers/mime_types.rb +4 -0
  47. data/test/dummy/config/initializers/session_store.rb +3 -0
  48. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  49. data/test/dummy/config/locales/devise.en.yml +60 -0
  50. data/test/dummy/config/locales/en.yml +23 -0
  51. data/test/dummy/config/routes.rb +7 -0
  52. data/test/dummy/config/secrets.yml +22 -0
  53. data/test/dummy/db/migrate/20150320103707_devise_create_users.rb +42 -0
  54. data/test/dummy/db/schema.rb +34 -0
  55. data/test/dummy/public/404.html +67 -0
  56. data/test/dummy/public/422.html +67 -0
  57. data/test/dummy/public/500.html +66 -0
  58. data/test/dummy/public/favicon.ico +0 -0
  59. data/test/dummy/test/fixtures/users.yml +11 -0
  60. data/test/dummy/test/models/user_test.rb +7 -0
  61. data/test/integration/navigation_test.rb +25 -0
  62. data/test/support/helpers.rb +40 -0
  63. data/test/test_helper.rb +46 -0
  64. 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="&#x2f;frame&#x2f;static&#x2f;js&#x2f;lib&#x2f;html5shiv.js&#x3f;v&#x3d;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="&#x2f;frame&#x2f;static&#x2f;js&#x2f;page&#x2f;enroll_new&#x2f;quirks.js&#x3f;v&#x3d;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,5 @@
1
+ module Devise
2
+ module DuoSecurity
3
+ VERSION = "0.0.7"
4
+ end
5
+ 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,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :devise_duo_security do
3
+ # # Task goes here
4
+ # 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
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 4.x"
4
+ gem 'jquery-rails', '~> 4.0.0'
5
+ gem "sqlite3"
6
+ gem "rake"
7
+ gem 'dotenv-rails'
8
+
9
+ gem "devise", "~> 3.x"
10
+ gem "devise_duo_sec", :path => "../.."