devise_duo_sec 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b03257e334c11b9b162063479a449311b33ec6ae
4
+ data.tar.gz: 468bb188c5f389516d1d5e7d2de8c1384482df9a
5
+ SHA512:
6
+ metadata.gz: a2805b48ae2b8ab0acc6cb98a547edffb0ac28b38dfb446a0e6e325ca894c44d59e03e8d2d774ba4767f69e6d56c44a34d83a28a6e5622755eae969c25de3e80
7
+ data.tar.gz: 2fd5895337e55b082907e8fa3c86dd1c022ede1aa83c917518c0c6948cd8620fda504ffd51595eb34f843f28ed475baecf7901e5e2545b023d701a1fa5aa0516
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Greg Molnar
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.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = Devise::DuoSecurity
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,34 @@
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 = 'Devise::DuoSecurity'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Duo Web SDK v2
3
+ * Copyright 2015, Duo Security
4
+ */
5
+ window.Duo = (function(document, window) {
6
+ var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
7
+ var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
8
+
9
+ var iframeId = 'duo_iframe',
10
+ postAction = '',
11
+ postArgument = 'sig_response',
12
+ host,
13
+ sigRequest,
14
+ duoSig,
15
+ appSig,
16
+ iframe,
17
+ submitCallback;
18
+
19
+ function throwError(message, url) {
20
+ throw new Error(
21
+ 'Duo Web SDK error: ' + message +
22
+ (url ? ('\n' + 'See ' + url + ' for more information') : '')
23
+ );
24
+ }
25
+
26
+ function hyphenize(str) {
27
+ return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
28
+ }
29
+
30
+ // cross-browser data attributes
31
+ function getDataAttribute(element, name) {
32
+ if ('dataset' in element) {
33
+ return element.dataset[name];
34
+ } else {
35
+ return element.getAttribute('data-' + hyphenize(name));
36
+ }
37
+ }
38
+
39
+ // cross-browser event binding/unbinding
40
+ function on(context, event, fallbackEvent, callback) {
41
+ if ('addEventListener' in window) {
42
+ context.addEventListener(event, callback, false);
43
+ } else {
44
+ context.attachEvent(fallbackEvent, callback);
45
+ }
46
+ }
47
+
48
+ function off(context, event, fallbackEvent, callback) {
49
+ if ('removeEventListener' in window) {
50
+ context.removeEventListener(event, callback, false);
51
+ } else {
52
+ context.detachEvent(fallbackEvent, callback);
53
+ }
54
+ }
55
+
56
+ function onReady(callback) {
57
+ on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
58
+ }
59
+
60
+ function offReady(callback) {
61
+ off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
62
+ }
63
+
64
+ function onMessage(callback) {
65
+ on(window, 'message', 'onmessage', callback);
66
+ }
67
+
68
+ function offMessage(callback) {
69
+ off(window, 'message', 'onmessage', callback);
70
+ }
71
+
72
+ /**
73
+ * Parse the sig_request parameter, throwing errors if the token contains
74
+ * a server error or if the token is invalid.
75
+ *
76
+ * @param {String} sig Request token
77
+ */
78
+ function parseSigRequest(sig) {
79
+ if (!sig) {
80
+ // nothing to do
81
+ return;
82
+ }
83
+
84
+ // see if the token contains an error, throwing it if it does
85
+ if (sig.indexOf('ERR|') === 0) {
86
+ throwError(sig.split('|')[1]);
87
+ }
88
+
89
+ // validate the token
90
+ if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
91
+ throwError(
92
+ 'Duo was given a bad token. This might indicate a configuration ' +
93
+ 'problem with one of Duo\'s client libraries.',
94
+ 'https://www.duosecurity.com/docs/duoweb#first-steps'
95
+ );
96
+ }
97
+
98
+ var sigParts = sig.split(':');
99
+
100
+ // hang on to the token, and the parsed duo and app sigs
101
+ sigRequest = sig;
102
+ duoSig = sigParts[0];
103
+ appSig = sigParts[1];
104
+
105
+ return {
106
+ sigRequest: sig,
107
+ duoSig: sigParts[0],
108
+ appSig: sigParts[1]
109
+ };
110
+ }
111
+
112
+ /**
113
+ * This function is set up to run when the DOM is ready, if the iframe was
114
+ * not available during `init`.
115
+ */
116
+ function onDOMReady() {
117
+ iframe = document.getElementById(iframeId);
118
+
119
+ if (!iframe) {
120
+ throw new Error(
121
+ 'This page does not contain an iframe for Duo to use.' +
122
+ 'Add an element like <iframe id="duo_iframe"></iframe> ' +
123
+ 'to this page. ' +
124
+ 'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' +
125
+ 'for more information.'
126
+ );
127
+ }
128
+
129
+ // we've got an iframe, away we go!
130
+ ready();
131
+
132
+ // always clean up after yourself
133
+ offReady(onDOMReady);
134
+ }
135
+
136
+ /**
137
+ * Validate that a MessageEvent came from the Duo service, and that it
138
+ * is a properly formatted payload.
139
+ *
140
+ * The Google Chrome sign-in page injects some JS into pages that also
141
+ * make use of postMessage, so we need to do additional validation above
142
+ * and beyond the origin.
143
+ *
144
+ * @param {MessageEvent} event Message received via postMessage
145
+ */
146
+ function isDuoMessage(event) {
147
+ return Boolean(
148
+ event.origin === ('https://' + host) &&
149
+ typeof event.data === 'string' &&
150
+ (
151
+ event.data.match(DUO_MESSAGE_FORMAT) ||
152
+ event.data.match(DUO_ERROR_FORMAT)
153
+ )
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Validate the request token and prepare for the iframe to become ready.
159
+ *
160
+ * All options below can be passed into an options hash to `Duo.init`, or
161
+ * specified on the iframe using `data-` attributes.
162
+ *
163
+ * Options specified using the options hash will take precedence over
164
+ * `data-` attributes.
165
+ *
166
+ * Example using options hash:
167
+ * ```javascript
168
+ * Duo.init({
169
+ * iframe: "some_other_id",
170
+ * host: "api-main.duo.test",
171
+ * sig_request: "...",
172
+ * post_action: "/auth",
173
+ * post_argument: "resp"
174
+ * });
175
+ * ```
176
+ *
177
+ * Example using `data-` attributes:
178
+ * ```
179
+ * <iframe id="duo_iframe"
180
+ * data-host="api-main.duo.test"
181
+ * data-sig-request="..."
182
+ * data-post-action="/auth"
183
+ * data-post-argument="resp"
184
+ * >
185
+ * </iframe>
186
+ * ```
187
+ *
188
+ * @param {Object} options
189
+ * @param {String} options.iframe The iframe, or id of an iframe to set up
190
+ * @param {String} options.host Hostname
191
+ * @param {String} options.sig_request Request token
192
+ * @param {String} [options.post_action=''] URL to POST back to after successful auth
193
+ * @param {String} [options.post_argument='sig_response'] Parameter name to use for response token
194
+ * @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute
195
+ * the callback function with reference to the "duo_form" form object
196
+ * submit_callback can be used to prevent the webpage from reloading.
197
+ */
198
+ function init(options) {
199
+ if (options) {
200
+ if (options.host) {
201
+ host = options.host;
202
+ }
203
+
204
+ if (options.sig_request) {
205
+ parseSigRequest(options.sig_request);
206
+ }
207
+
208
+ if (options.post_action) {
209
+ postAction = options.post_action;
210
+ }
211
+
212
+ if (options.post_argument) {
213
+ postArgument = options.post_argument;
214
+ }
215
+
216
+ if (options.iframe) {
217
+ if ('tagName' in options.iframe) {
218
+ iframe = options.iframe;
219
+ } else if (typeof options.iframe === 'string') {
220
+ iframeId = options.iframe;
221
+ }
222
+ }
223
+
224
+ if (typeof options.submit_callback === 'function') {
225
+ submitCallback = options.submit_callback;
226
+ }
227
+ }
228
+
229
+ // if we were given an iframe, no need to wait for the rest of the DOM
230
+ if (iframe) {
231
+ ready();
232
+ } else {
233
+ // try to find the iframe in the DOM
234
+ iframe = document.getElementById(iframeId);
235
+
236
+ // iframe is in the DOM, away we go!
237
+ if (iframe) {
238
+ ready();
239
+ } else {
240
+ // wait until the DOM is ready, then try again
241
+ onReady(onDOMReady);
242
+ }
243
+ }
244
+
245
+ // always clean up after yourself!
246
+ offReady(init);
247
+ }
248
+
249
+ /**
250
+ * This function is called when a message was received from another domain
251
+ * using the `postMessage` API. Check that the event came from the Duo
252
+ * service domain, and that the message is a properly formatted payload,
253
+ * then perform the post back to the primary service.
254
+ *
255
+ * @param event Event object (contains origin and data)
256
+ */
257
+ function onReceivedMessage(event) {
258
+ if (isDuoMessage(event)) {
259
+ // the event came from duo, do the post back
260
+ doPostBack(event.data);
261
+
262
+ // always clean up after yourself!
263
+ offMessage(onReceivedMessage);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Point the iframe at Duo, then wait for it to postMessage back to us.
269
+ */
270
+ function ready() {
271
+ if (!host) {
272
+ host = getDataAttribute(iframe, 'host');
273
+
274
+ if (!host) {
275
+ throwError(
276
+ 'No API hostname is given for Duo to use. Be sure to pass ' +
277
+ 'a `host` parameter to Duo.init, or through the `data-host` ' +
278
+ 'attribute on the iframe element.',
279
+ 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
280
+ );
281
+ }
282
+ }
283
+
284
+ if (!duoSig || !appSig) {
285
+ parseSigRequest(getDataAttribute(iframe, 'sigRequest'));
286
+
287
+ if (!duoSig || !appSig) {
288
+ throwError(
289
+ 'No valid signed request is given. Be sure to give the ' +
290
+ '`sig_request` parameter to Duo.init, or use the ' +
291
+ '`data-sig-request` attribute on the iframe element.',
292
+ 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
293
+ );
294
+ }
295
+ }
296
+
297
+ // if postAction/Argument are defaults, see if they are specified
298
+ // as data attributes on the iframe
299
+ if (postAction === '') {
300
+ postAction = getDataAttribute(iframe, 'postAction') || postAction;
301
+ }
302
+
303
+ if (postArgument === 'sig_response') {
304
+ postArgument = getDataAttribute(iframe, 'postArgument') || postArgument;
305
+ }
306
+
307
+ // point the iframe at Duo
308
+ iframe.src = [
309
+ 'https://', host, '/frame/web/v1/auth?tx=', duoSig,
310
+ '&parent=', encodeURIComponent(document.location.href),
311
+ '&v=2.3'
312
+ ].join('');
313
+
314
+ // listen for the 'message' event
315
+ onMessage(onReceivedMessage);
316
+ }
317
+
318
+ /**
319
+ * We received a postMessage from Duo. POST back to the primary service
320
+ * with the response token, and any additional user-supplied parameters
321
+ * given in form#duo_form.
322
+ */
323
+ function doPostBack(response) {
324
+ // create a hidden input to contain the response token
325
+ var input = document.createElement('input');
326
+ input.type = 'hidden';
327
+ input.name = postArgument;
328
+ input.value = response + ':' + appSig;
329
+
330
+ // user may supply their own form with additional inputs
331
+ var form = document.getElementById('duo_form');
332
+
333
+ // if the form doesn't exist, create one
334
+ if (!form) {
335
+ form = document.createElement('form');
336
+
337
+ // insert the new form after the iframe
338
+ iframe.parentElement.insertBefore(form, iframe.nextSibling);
339
+ }
340
+
341
+ // make sure we are actually posting to the right place
342
+ form.method = 'POST';
343
+ form.action = postAction;
344
+
345
+ // add the response token input to the form
346
+ form.appendChild(input);
347
+
348
+ // away we go!
349
+ if (typeof submitCallback === "function") {
350
+ submitCallback.call(null, form);
351
+ } else {
352
+ form.submit();
353
+ }
354
+ }
355
+
356
+ // when the DOM is ready, initialize
357
+ // note that this will get cleaned up if the user calls init directly!
358
+ onReady(init);
359
+
360
+ return {
361
+ init: init,
362
+ _parseSigRequest: parseSigRequest,
363
+ _isDuoMessage: isDuoMessage,
364
+ _doPostBack: doPostBack
365
+ };
366
+ }(document, window));
@@ -0,0 +1,10 @@
1
+ body {
2
+ text-align: center;
3
+ }
4
+
5
+ iframe {
6
+ width: 100%;
7
+ min-width: 304px;
8
+ max-width: 620px;
9
+ height: 330px;
10
+ }