diff --git a/demos/flask/app.py b/demos/flask/app.py index c2ebd41..028da67 100644 --- a/demos/flask/app.py +++ b/demos/flask/app.py @@ -3,9 +3,9 @@ import sys import argparse import flask -import duo_web +import duo_universal -app = flask.Flask(__name__, static_url_path='', static_folder='static') +app = flask.Flask(__name__) app.secret_key = os.urandom(32) @app.route("/", methods=['GET']) @@ -17,29 +17,48 @@ def do_GET(): if username is None: return 'user query parameter is required', 400 - sig_request = duo_web.sign_request(app.ikey, app.skey, app.akey, username) - return flask.render_template('index.html', host=app.host, sig_request=sig_request) + try: + app.duo_client.health_check() + except duo_universal.DuoException: + return 'Duo health check failed. Logged in without 2FA as %s.' % username + # alternatively: return 'Duo health check failed, denying login.' -@app.route("/", methods=['POST']) -def do_POST(): - sig_response = flask.request.form.get('sig_response') - if sig_response is None: - return 'sig_response post parameter is required', 400 - user = duo_web.verify_response( - app.ikey, app.skey, app.akey, sig_response) + state = app.duo_client.generate_state() + prompt_uri = app.duo_client.create_auth_url(username, state) - if user is None: + flask.session['state'] = state + flask.session['username'] = username + + return flask.redirect(prompt_uri) + +@app.route("/duo-callback", methods=['GET']) +def do_duo_callback(): + state = flask.session['state'] + username = flask.session['username'] + + if (state is None) or (username is None) or (state != flask.request.args.get('state')): + return 'Invalid state or username' + + duo_code = flask.request.args.get('duo_code') + if duo_code is None: + return 'duo_code post parameter is required', 400 + + try: + app.duo_client.exchange_authorization_code_for_2fa_result(duo_code, username) + except duo_universal.DuoException: return 'Did not authenticate with Duo.'.encode('utf-8') - return ('Authenticated with Duo as %s.' % user).encode('utf-8') + return ('Authenticated with Duo as %s.' % username).encode('utf-8') -def main(ikey, skey, akey, host, port=8080): +def main(client_id, client_secret, host, redirect_uri, port=8080): port = int(port) - app.ikey = ikey - app.skey = skey - app.akey = akey - app.host = host + app.duo_client = duo_universal.Client( + client_id=client_id, + client_secret=client_secret, + host=host, + redirect_uri=redirect_uri + ) print("Visit the root URL with a 'user' argument, e.g.") print("'http://localhost:%d/?user=myname'." % port) app.run(host="0.0.0.0", port=8080) diff --git a/demos/flask/duo.conf b/demos/flask/duo.conf index 1be587d..e178c4a 100644 --- a/demos/flask/duo.conf +++ b/demos/flask/duo.conf @@ -2,7 +2,7 @@ [duo] -ikey = ikey -skey = skey -akey = akey +client_id = client_id +client_secret = client_secret host = host +redirect_uri = http://localhost:8080/duo-callback diff --git a/demos/flask/requirements.txt b/demos/flask/requirements.txt index 6fc9498..1cd3bdd 100644 --- a/demos/flask/requirements.txt +++ b/demos/flask/requirements.txt @@ -1,3 +1,3 @@ -duo-web +duo-universal flask six==1.14.0 diff --git a/demos/flask/static/Duo-Frame.css b/demos/flask/static/Duo-Frame.css deleted file mode 100644 index 9a1c8f9..0000000 --- a/demos/flask/static/Duo-Frame.css +++ /dev/null @@ -1,15 +0,0 @@ -body { - text-align: center; - font-family: "Helvetica Neue", helvetica, arial, sans-serif; -} - -h1 { - font-weight: 200; -} - -iframe { - width: 100%; - min-width: 304px; - max-width: 620px; - height: 330px; -} diff --git a/demos/flask/static/Duo-Web-v2.js b/demos/flask/static/Duo-Web-v2.js deleted file mode 100644 index 06056cf..0000000 --- a/demos/flask/static/Duo-Web-v2.js +++ /dev/null @@ -1,578 +0,0 @@ -/** - * Duo Web SDK v2 - * Copyright 2021, Duo Security - */ - -(function (root, factory) { - /*eslint-disable */ - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define([], factory); - /*eslint-enable */ - } else if (typeof module === 'object' && module.exports) { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - var Duo = factory(); - // If the Javascript was loaded via a script tag, attempt to autoload - // the frame. - Duo._onReady(Duo.init); - - // Attach Duo to the `window` object - root.Duo = Duo; - } -}(this, function() { - var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/; - var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/; - var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/; - var VALID_OPEN_WINDOW_DOMAINS = [ - 'duo.com', - 'duosecurity.com', - 'duomobile.s3-us-west-1.amazonaws.com' - ]; - - var postAction, - postArgument, - host, - sigRequest, - duoSig, - appSig, - iframe, - submitCallback; - - // We use this function instead of setting initial values in the var - // declarations to make sure the initial values and subsequent - // re-initializations are always the same. - initializeStatefulVariables(); - - /** - * Set local variables to whatever they should be before you call init(). - */ - function initializeStatefulVariables() { - postAction = ''; - postArgument = 'sig_response'; - host = undefined; - sigRequest = undefined; - duoSig = undefined; - appSig = undefined; - iframe = undefined; - submitCallback = undefined; - } - - function throwError(message, givenUrl) { - var url = ( - givenUrl || - 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' - ); - throw new Error( - 'Duo Web SDK error: ' + message + - (url ? ('\n' + 'See ' + url + ' for more information') : '') - ); - } - - function hyphenize(str) { - return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase(); - } - - // cross-browser data attributes - function getDataAttribute(element, name) { - if ('dataset' in element) { - return element.dataset[name]; - } else { - return element.getAttribute('data-' + hyphenize(name)); - } - } - - // cross-browser event binding/unbinding - function on(context, event, fallbackEvent, callback) { - if ('addEventListener' in window) { - context.addEventListener(event, callback, false); - } else { - context.attachEvent(fallbackEvent, callback); - } - } - - function off(context, event, fallbackEvent, callback) { - if ('removeEventListener' in window) { - context.removeEventListener(event, callback, false); - } else { - context.detachEvent(fallbackEvent, callback); - } - } - - function onReady(callback) { - on(document, 'DOMContentLoaded', 'onreadystatechange', callback); - } - - function offReady(callback) { - off(document, 'DOMContentLoaded', 'onreadystatechange', callback); - } - - function onMessage(callback) { - on(window, 'message', 'onmessage', callback); - } - - function offMessage(callback) { - off(window, 'message', 'onmessage', callback); - } - - /** - * Parse the sig_request parameter, throwing errors if the token contains - * a server error or if the token is invalid. - * - * @param {String} sig Request token - */ - function parseSigRequest(sig) { - if (!sig) { - // nothing to do - return; - } - - // see if the token contains an error, throwing it if it does - if (sig.indexOf('ERR|') === 0) { - throwError(sig.split('|')[1]); - } - - // validate the token - if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) { - throwError( - 'Duo was given a bad token. This might indicate a configuration ' + - 'problem with one of Duo\'s client libraries.' - ); - } - - var sigParts = sig.split(':'); - - // hang on to the token, and the parsed duo and app sigs - sigRequest = sig; - duoSig = sigParts[0]; - appSig = sigParts[1]; - - return { - sigRequest: sig, - duoSig: sigParts[0], - appSig: sigParts[1] - }; - } - - /** - * Validate that a MessageEvent came from the Duo service, and that it - * is a properly formatted payload. - * - * The Google Chrome sign-in page injects some JS into pages that also - * make use of postMessage, so we need to do additional validation above - * and beyond the origin. - * - * @param {MessageEvent} event Message received via postMessage - */ - function isDuoMessage(event) { - return Boolean( - event.origin === ('https://' + host) && - typeof event.data === 'string' && - ( - event.data.match(DUO_MESSAGE_FORMAT) || - event.data.match(DUO_ERROR_FORMAT) || - event.data.match(DUO_OPEN_WINDOW_FORMAT) - ) - ); - } - - /** - * Validate the request token and prepare for the iframe to become ready. - * - * All options below can be passed into an options hash to `Duo.init`, or - * specified on the iframe using `data-` attributes. - * - * Options specified using the options hash will take precedence over - * `data-` attributes. - * - * Example using options hash: - * ```javascript - * Duo.init({ - * iframe: "some_other_id", - * host: "api-main.duo.test", - * sig_request: "...", - * post_action: "/auth", - * post_argument: "resp" - * }); - * ``` - * - * Example using `data-` attributes: - * ```html - * - * ``` - * - * Some browsers (especially embedded browsers) don't like it when the Duo - * Web SDK changes the `src` attribute on the iframe. To prevent this, there - * is an alternative way to use the Duo Web SDK: - * - * Add a div (or any other container element) instead of an iframe to the - * DOM with an id of "duo_iframe", or pass that element to the - * `iframeContainer` parameter of `Duo.init`. An iframe will be created and - * inserted into that container element, preventing `src` change related - * bugs. WARNING: All other elements in the container will be deleted. - * - * The `iframeAttributes` parameter of `Duo.init` is available to set any - * attributes on the inserted iframe if the Duo Web SDK is inserting the - * iframe. For details, see the parameter documentation below. - * - * @param {Object} options - * @param {String} options.host - Hostname for the Duo Prompt. - * @param {String} options.sig_request - Request token. - * @param {String|HTMLElement} [options.iframe] - The iframe, or id of an - * iframe that will be used for the Duo Prompt. If you don't provide - * this or the `iframeContainer` parameter the Duo Web SDK will default - * to using whatever element has an id of "duo_iframe". - * @param {String|HTMLElement} [options.iframeContainer] - The element you - * want the Duo Prompt inserted into, or the id of that element. - * Anything inside this element will be deleted and replaced with an - * iframe hosting the Duo prompt. If you don't provide this or the - * `iframe` parameter the Duo Web SDK will default to using whatever - * element has an id of "duo_iframe". - * @param {Object} [options.iframeAttributes] - Object with names and - * values coresponding to attributes you want added to the Duo Prompt - * iframe, like `title`, `width` and `allow`. WARNING: this parameter - * only works if you use the `iframeContainer` parameter or add an id - * of "duo_iframe" to an element that isn't an iframe. If you have - * added an iframe to the DOM yourself, you should set those attributes - * directly on the iframe. - * @param {String} [options.post_action=''] - URL to POST back to after a - * successful auth. - * @param {String} [options.post_argument='sig_response'] - Parameter name - * to use for response token. - * @param {Function} [options.submit_callback] - If provided, the Duo Web - * SDK will not submit the form. Instead it will execute this callback - * function passing in a reference to the "duo_form" form object. - * `submit_callback`` can be used to prevent the webpage from reloading. - */ - function init(options) { - // If init() is called more than once we have to reset all the local - // variables to ensure init() will work the same way every time. This - // helps people making single page applications. SPAs may periodically - // remove the iframe and add a new one that has to be initialized. - initializeStatefulVariables(); - - if (options) { - if (options.host) { - host = options.host; - } - - if (options.sig_request) { - parseSigRequest(options.sig_request); - } - - if (options.post_action) { - postAction = options.post_action; - } - - if (options.post_argument) { - postArgument = options.post_argument; - } - - if (typeof options.submit_callback === 'function') { - submitCallback = options.submit_callback; - } - } - - var promptElement = getPromptElement(options); - if (promptElement) { - // If we can get the element that will host the prompt, set it. - ready(promptElement, options.iframeAttributes || {}); - } else { - // If the element that will host the prompt isn't available yet, set - // it up after the DOM finishes loading. - asyncReady(options); - } - - // always clean up after yourself! - offReady(init); - } - - /** - * Given the options from init(), get the iframe or iframe container that - * should be used for the Duo Prompt. Returns `null` if nothing was found. - */ - function getPromptElement(options) { - var result; - - if (options.iframe && options.iframeContainer) { - throwError( - 'Passing both `iframe` and `iframeContainer` arguments at the' + - ' same time is not allowed.' - ); - } else if (options.iframe) { - // If we are getting an iframe, try to get it and raise if the - // element we find is NOT an iframe. - result = getUserDefinedElement(options.iframe); - validateIframe(result); - } else if (options.iframeContainer) { - result = getUserDefinedElement(options.iframeContainer); - validateIframeContainer(result); - } else { - result = document.getElementById('duo_iframe'); - } - - return result; - } - - /** - * When given an HTMLElement, return it. When given a string, get an element - * with that id, else return null. - */ - function getUserDefinedElement(object) { - if (object.tagName) { - return object; - } else if (typeof object == 'string') { - return document.getElementById(object); - } - return null; - } - - /** - * Check if the given thing is an iframe. - */ - function isIframe(element) { - return ( - element && - element.tagName && - element.tagName.toLowerCase() === 'iframe' - ); - } - - /** - * Throw an error if we are given an element that is NOT an iframe. - */ - function validateIframe(element) { - if (element && !isIframe(element)) { - throwError( - '`iframe` only accepts an iframe element or the id of an' + - ' iframe. To use a non-iframe element, use the' + - ' `iframeContainer` argument.' - ); - } - } - - /** - * Throw an error if we are given an element that IS an iframe instead of an - * element that we can insert an iframe into. - */ - function validateIframeContainer(element) { - if (element && isIframe(element)) { - throwError( - '`iframeContainer` only accepts a non-iframe element or the' + - ' id of a non-iframe. To use a non-iframe element, use the' + - ' `iframeContainer` argument on Duo.init().' - ); - } - } - - /** - * Generate the URL that goes to the Duo Prompt. - */ - function generateIframeSrc() { - return [ - 'https://', host, '/frame/web/v1/auth?tx=', encodeURIComponent(duoSig), - '&parent=', encodeURIComponent(document.location.href), - '&v=2.9' - ].join(''); - } - - /** - * This function is called when a message was received from another domain - * using the `postMessage` API. Check that the event came from the Duo - * service domain, and that the message is a properly formatted payload, - * then perform the post back to the primary service. - * - * @param event Event object (contains origin and data) - */ - function onReceivedMessage(event) { - if (isDuoMessage(event)) { - if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) { - var url = event.data.substring("DUO_OPEN_WINDOW|".length); - if (isValidUrlToOpen(url)) { - // Open the URL that comes after the DUO_WINDOW_OPEN token. - window.open(url, "_self"); - } - } - else { - // the event came from duo, do the post back - doPostBack(event.data); - - // always clean up after yourself! - offMessage(onReceivedMessage); - } - } - } - - /** - * Validate that this passed in URL is one that we will actually allow to - * be opened. - * @param url String URL that the message poster wants to open - * @returns {boolean} true if we allow this url to be opened in the window - */ - function isValidUrlToOpen(url) { - if (!url) { - return false; - } - - var parser = document.createElement('a'); - parser.href = url; - - if (parser.protocol === "duotrustedendpoints:") { - return true; - } else if (parser.protocol !== "https:") { - return false; - } - - for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) { - if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) || - parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) { - return true; - } - } - return false; - } - - /** - * Register a callback to call ready() after the DOM has loaded. - */ - function asyncReady(options) { - var callback = function() { - var promptElement = getPromptElement(options); - if (!promptElement) { - throwError( - 'This page does not contain an iframe for Duo to use.' + - ' Add an element like' + - ' to this page.' - ); - } - - ready(promptElement, options.iframeAttributes || {}); - - // Always clean up after yourself. - offReady(callback) - }; - - onReady(callback); - } - - /** - * Point the iframe at Duo, then wait for it to postMessage back to us. - */ - function ready(promptElement, iframeAttributes) { - if (!host) { - host = getDataAttribute(promptElement, 'host'); - - if (!host) { - throwError( - '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.' - ); - } - } - - if (!duoSig || !appSig) { - parseSigRequest(getDataAttribute(promptElement, 'sigRequest')); - - if (!duoSig || !appSig) { - throwError( - '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.' - ); - } - } - - // if postAction/Argument are defaults, see if they are specified - // as data attributes on the iframe - if (postAction === '') { - postAction = getDataAttribute(promptElement, 'postAction') || postAction; - } - - if (postArgument === 'sig_response') { - postArgument = getDataAttribute(promptElement, 'postArgument') || postArgument; - } - - if (isIframe(promptElement)) { - iframe = promptElement; - iframe.src = generateIframeSrc(); - } else { - // If given a container to put an iframe in, clean out any children - // child elements in case `init()` was called more than once. - while (promptElement.firstChild) { - // We call `removeChild()` instead of doing `innerHTML = ""` - // to make sure we unbind any events. - promptElement.removeChild(promptElement.firstChild); - } - - iframe = document.createElement('iframe'); - - // Set the src and all other attributes on the new iframe. - iframeAttributes['src'] = generateIframeSrc(); - for (var name in iframeAttributes) { - iframe.setAttribute(name, iframeAttributes[name]); - } - - promptElement.appendChild(iframe); - } - - // listen for the 'message' event - onMessage(onReceivedMessage); - } - - /** - * We received a postMessage from Duo. POST back to the primary service - * with the response token, and any additional user-supplied parameters - * given in form#duo_form. - */ - function doPostBack(response) { - // create a hidden input to contain the response token - var input = document.createElement('input'); - input.type = 'hidden'; - input.name = postArgument; - input.value = response + ':' + appSig; - - // user may supply their own form with additional inputs - var form = document.getElementById('duo_form'); - - // if the form doesn't exist, create one - if (!form) { - form = document.createElement('form'); - - // insert the new form after the iframe - iframe.parentElement.insertBefore(form, iframe.nextSibling); - } - - // make sure we are actually posting to the right place - form.method = 'POST'; - form.action = postAction; - - // add the response token input to the form - form.appendChild(input); - - // away we go! - if (typeof submitCallback === "function") { - submitCallback.call(null, form); - } else { - form.submit(); - } - } - - return { - init: init, - _onReady: onReady, - _parseSigRequest: parseSigRequest, - _isDuoMessage: isDuoMessage, - _doPostBack: doPostBack - }; -})); diff --git a/demos/flask/templates/index.html b/demos/flask/templates/index.html deleted file mode 100644 index 61dc930..0000000 --- a/demos/flask/templates/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Duo Authentication Prompt - - - - - -

Duo Authentication Prompt

- - - - \ No newline at end of file