duo_web 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7c058b1950046167876439b51f1704b6352f255e
4
+ data.tar.gz: cb1a60d911a9874e70eefcadb16a4273e339d8f9
5
+ SHA512:
6
+ metadata.gz: c57215b09fc5cc0135347425b65016e8b086962302677faaa12ca09d0c2481f9208228e8a96959ec1fff010dd79e980800dd30ed8223b830bc547603a3ba0d82
7
+ data.tar.gz: c2733a950b847ab7e66e330d8207d0d64d1676b76cc5a5add8fe4bb0893d98f6a6c2b75e4ed6fe8f6a3a6cd8e18f0f13ac34c778869017cab31cf60452ef157b
data/js/Duo-Web-v2.js ADDED
@@ -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=', document.location.href,
311
+ '&v=2.2'
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 @@
1
+ window.Duo=function(e,t){var i=/^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;var o=/^ERR\|[\w\s\.\(\)]+$/;var n="duo_iframe",a="",s="sig_response",r,f,u,d,m,c;function h(e,t){throw new Error("Duo Web SDK error: "+e+(t?"\n"+"See "+t+" for more information":""))}function g(e){return e.replace(/([a-z])([A-Z])/,"$1-$2").toLowerCase()}function l(e,t){if("dataset"in e){return e.dataset[t]}else{return e.getAttribute("data-"+g(t))}}function p(e,i,o,n){if("addEventListener"in t){e.addEventListener(i,n,false)}else{e.attachEvent(o,n)}}function w(e,i,o,n){if("removeEventListener"in t){e.removeEventListener(i,n,false)}else{e.detachEvent(o,n)}}function v(t){p(e,"DOMContentLoaded","onreadystatechange",t)}function b(t){w(e,"DOMContentLoaded","onreadystatechange",t)}function E(e){p(t,"message","onmessage",e)}function _(e){w(t,"message","onmessage",e)}function y(e){if(!e){return}if(e.indexOf("ERR|")===0){h(e.split("|")[1])}if(e.indexOf(":")===-1||e.split(":").length!==2){h("Duo was given a bad token. This might indicate a configuration "+"problem with one of Duo's client libraries.","https://www.duosecurity.com/docs/duoweb#first-steps")}var t=e.split(":");f=e;u=t[0];d=t[1];return{sigRequest:e,duoSig:t[0],appSig:t[1]}}function D(){m=e.getElementById(n);if(!m){throw new Error("This page does not contain an iframe for Duo to use."+'Add an element like <iframe id="duo_iframe"></iframe> '+"to this page. "+"See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe "+"for more information.")}B();b(D)}function A(e){return Boolean(e.origin==="https://"+r&&typeof e.data==="string"&&(e.data.match(i)||e.data.match(o)))}function L(t){if(t){if(t.host){r=t.host}if(t.sig_request){y(t.sig_request)}if(t.post_action){a=t.post_action}if(t.post_argument){s=t.post_argument}if(t.iframe){if("tagName"in t.iframe){m=t.iframe}else if(typeof t.iframe==="string"){n=t.iframe}}if(typeof t.submit_callback==="function"){c=t.submit_callback}}if(m){B()}else{m=e.getElementById(n);if(m){B()}else{v(D)}}b(L)}function q(e){if(A(e)){R(e.data);_(q)}}function B(){if(!r){r=l(m,"host");if(!r){h("No API hostname is given for Duo to use. Be sure to pass "+"a `host` parameter to Duo.init, or through the `data-host` "+"attribute on the iframe element.","https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe")}}if(!u||!d){y(l(m,"sigRequest"));if(!u||!d){h("No valid signed request is given. Be sure to give the "+"`sig_request` parameter to Duo.init, or use the "+"`data-sig-request` attribute on the iframe element.","https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe")}}if(a===""){a=l(m,"postAction")||a}if(s==="sig_response"){s=l(m,"postArgument")||s}m.src=["https://",r,"/frame/web/v1/auth?tx=",u,"&parent=",e.location.href,"&v=2.2"].join("");E(q)}function R(t){var i=e.createElement("input");i.type="hidden";i.name=s;i.value=t+":"+d;var o=e.getElementById("duo_form");if(!o){o=e.createElement("form");m.parentElement.insertBefore(o,m.nextSibling)}o.method="POST";o.action=a;o.appendChild(i);if(typeof c==="function"){c.call(null,o)}else{o.submit()}}v(L);return{init:L,_parseSigRequest:y,_isDuoMessage:A,_doPostBack:R}}(document,window);
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
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duo_web
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Duo Security
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A Ruby implementation of the Duo Web SDK.
42
+ email: support@duo.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - js/Duo-Web-v2.js
48
+ - js/Duo-Web-v2.min.js
49
+ - lib/duo_web.rb
50
+ homepage: https://github.com/duosecurity/duo_ruby
51
+ licenses:
52
+ - BSD-3-Clause
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 2.5.1
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Duo Web Ruby
74
+ test_files: []