duo_web 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []