Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffreyparker committed Sep 16, 2020
0 parents commit c710948
Show file tree
Hide file tree
Showing 27 changed files with 1,684 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
# Ignore line too long errors
ignore = E501
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
*.egg-info*
*.pyc
*.pyo
*.swp
*~
.env
.gitconfig
.idea
__pycache__
build
dist
env/
MANIFEST
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: python
python:
- '2.7'
- '3.5'
- '3.6'
- '3.7'
- '3.8'
install:
- pip install -r requirements.txt
- pip install -r tests/requirements.txt
script:
- flake8
- nose2
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (c) 2020, Duo Security, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Duo Universal Python SDK

This SDK allows a web developer to quickly add Duo's interactive, self-service, two-factor authentication to any Python web login form. Both Python 2 and Python 3 are supported.

What's here:
* `duo_universal` - The Python Duo SDK for interacting with the Duo Universal Prompt
* `demo` - An example web application with Duo integrated
* `tests` - Test cases

## Getting Started
To use the SDK in your existing development environment, install it from pypi (https://pypi.org/project/duo_universal).
```
pip install duo_universal
```
Once it's installed, see our developer documentation at https://duo.com/docs/duoweb and `demo/app.py` in this repo for guidance on integrating Duo 2FA into your web application.

## Contribute
To contribute, fork this repo and make a pull request with your changes when they're ready.

If you're not already working from a dedicated development environment, it's recommended a virtual environment is used. Assuming a virtual environment named `env`, create and activate the environment:
```
# Python 3
python -m venv env
source env/bin/activate
# Python 2
virtualenv env
source env/bin/activate
```

Build and install the SDK form source:
```
pip install -r requirements.txt
pip install .
```

## Tests
Install the test requirements:
```
cd tests
pip install -r requirements.txt
```
Then run tests from the `test` directory:
```
# Run an individual test file
python <test_name>.py
# Run all tests with nose
nose2
# Run all tests with unittest
python -m unittest
```

## Lint
```
flake8
```

## Support

Please report any bugs, feature requests, or issues to us directly at support@duosecurity.com.

Thank you for using Duo!

https://duo.com/
22 changes: 22 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Duo Universal Python SDK Demo

A simple Python web application that serves a logon page integrated with Duo 2FA.

## Setup
The following steps assume the SDK is already installed and that you're working in your preferred environment. See the top-level README for installation and environment setup instructions.

Install the demo requirements:
```
cd demo
pip install -r requirements.txt
```

Then, create a `Web SDK` application in the Duo Admin Panel. See https://duo.com/docs/protecting-applications for more details.
## Using the App

1. Using the Client ID, Client Secret, and API Hostname for your `Web SDK` application, start the app.
```
DUO_CLIENT_ID=<client_id> DUO_CLIENT_SECRET=<client_secret> DUO_API_HOST=<api_host> python app.py
```
1. Navigate to http://localhost:8080.
1. Log in with the user you would like to enroll in Duo or with an already enrolled user (any password will work).
93 changes: 93 additions & 0 deletions demo/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import json
import os
import traceback

from flask import Flask, request, redirect, session, render_template

from duo_universal.client import Client, DuoException

duo_client = Client(
client_id=os.environ.get("DUO_CLIENT_ID"),
client_secret=os.environ.get("DUO_CLIENT_SECRET"),
host=os.environ.get("DUO_API_HOST"),
redirect_uri="http://localhost:8080/duo-callback"
)

duo_failmode = os.environ.get("DUO_FAILMODE", "open")

app = Flask(__name__)


@app.route("/", methods=['GET'])
def login():
return render_template("login.html", message="This is a demo")


@app.route("/", methods=['POST'])
def login_post():
"""
respond to HTTP POST with 2FA as long as health check passes
"""
try:
duo_client.health_check()
except DuoException:
traceback.print_exc()
if duo_failmode.upper() == "OPEN":
# If we're failing open errors in 2FA still allow for success
return render_template("success.html",
message="Login 'Successful', but 2FA Not Performed. Confirm Duo client/secret/host values are correct")
else:
# Otherwise the login fails and redirect user to the login page
return render_template("login.html",
message="2FA Unavailable. Confirm Duo client/secret/host values are correct")

username = request.form.get('username')

# Generate random string to act as a state for the exchange
state = duo_client.generate_state()
session['state'] = state
session['username'] = username
prompt_uri = duo_client.create_auth_url(username, state)

# Redirect to prompt URI which will redirect to the client's redirect URI
# after 2FA
return redirect(prompt_uri)


# This route URL must match the redirect_uri passed to the duo client
@app.route("/duo-callback")
def duo_callback():
if request.args.get('error'):
return "Got Error: {}".format(request.args)

# Get state to verify consistency and originality
state = request.args.get('state')

# Get authorization token to trade for 2FA
code = request.args.get('code')

if 'state' in session and 'username' in session:
saved_state = session['state']
username = session['username']
else:
# For flask, if url used to get to login.html is not localhost,
# (ex: 127.0.0.1) then the sessions will be different
# and the localhost session does not have the state
return render_template("login.html",
message="No saved state please login again")

# Ensure nonce matches from initial request
if state != saved_state:
return render_template("login.html",
message="Duo state does not match saved state")

decoded_token = duo_client.exchange_authorization_code_for_2fa_result(code, username)

# Exchange happened successfully so render success page
return render_template("success.html",
message=json.dumps(decoded_token, indent=2, sort_keys=True))


if __name__ == '__main__':
app.secret_key = os.urandom(32)
app.run(host="localhost", port=8080)
1 change: 1 addition & 0 deletions demo/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask==1.1.2
Binary file added demo/static/images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions demo/static/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
body, input {
font: 17px arial, sans-serif;
}

.content,
.input-form {
display: flex;
flex-direction: column;
align-items: center;
}

img {
padding: 20px;
height: 84px;
margin-top: 40px;
}

input {
width: 250px;
padding: 12px;
margin-top: 10px;
}

input[type=text] {
margin-left: 44px;
}

input[type=password] {
margin-left: 10px;
}

h3 {
margin-top: 23px;
}

pre.language-json {
font-size: 20px;
}

div.success {
margin-top: 20px;
}

pre.auth-token {
display: inline-block;
text-align: left;
}

button {
background-color: #6BBF4E;
color: white;
border: none;
padding: 9px 18px;
margin-top: 17px;
border-radius: 4px;
}
26 changes: 26 additions & 0 deletions demo/templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<html>
<body>
<div class="content">
<link rel="stylesheet" href='/static/style.css'>
<div class="logo">
<img src="/static/images/logo.png">
</div>
<div class="output">
<pre class="language-json"><code class="language-json" id="message">{{ message }}</code></pre>
</div>
<form class="input-form" action="/" method="POST">
<div class="form-group">
<label for="exampleInputEmail1"><b>Name:</b></label>
<input class="form-control" name="username" type="text" placeholder="username" id="exampleInputEmail1">
</div>
<div class="form-group">
<label for="exampleInputPassword1"><b>Password:</b></label>
<input class="form-control" type="password" placeholder="password" id="exampleInputPassword1">
</div>
<div class="buttons">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</body>
</html>
16 changes: 16 additions & 0 deletions demo/templates/success.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html>
<body>
<div class="content">
<link rel="stylesheet" href='/static/style.css'>
<div class="logo">
<img src="/static/images/logo.png">
</div>
<div class="auth-resp">
<h3><b>Auth Response:</b></h3>
</div>
<div class="success">
<pre class="auth-token"><code class="auth-token" id="message">{{ message }}</code></pre>
</div>
</div>
</body>
</html>
2 changes: 2 additions & 0 deletions duo_universal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from duo_universal.client import *
from duo_universal.version import __version__
Loading

0 comments on commit c710948

Please sign in to comment.