Skip to content
This repository was archived by the owner on Oct 2, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
2667199
Merge tag '1.0.5' into develop
egrim Aug 26, 2013
e8bd38e
Add version constant to library, bump version, and test
smholloway Sep 12, 2013
29acbcb
Create a Toopher User-Agent string
smholloway Sep 13, 2013
90d48dd
Add headers to the mock request method to fix broken tests
smholloway Sep 13, 2013
a9139dc
Add Python version to User-Agent string
smholloway Sep 16, 2013
1e63ba2
Update version number test to be more robust
smholloway Sep 16, 2013
b636bd6
Add Travis CI build status indicator
smholloway Sep 16, 2013
692e3a7
Merge pull request #15 from toopher/feature/add_version_number
smholloway Sep 17, 2013
973aa0c
Add zero-storage
trdarr Nov 18, 2013
5abb39e
Remove response pprinting
trdarr Nov 18, 2013
ad0de36
Add tests
trdarr Nov 18, 2013
2c980f5
Update README
trdarr Nov 18, 2013
38c6a87
Comply with discussed nomenclature
trdarr Nov 25, 2013
7edac57
Merge pull request #16 from toopher/feature/zero-storage
trdarr Nov 25, 2013
3c397f1
Feedback from @smholloway on PR 16
trdarr Dec 2, 2013
4007f8a
Fix some perl-specific readme verbiage
Dec 3, 2013
4d3260e
Update version number
trdarr Dec 2, 2013
e19c35a
Replace python-oauth2 with Requests
trdarr Dec 3, 2013
d3f0987
Add requirements.txt
trdarr Dec 3, 2013
b5bf1db
Pyflakes
trdarr Dec 4, 2013
b90bb41
Update tests to use Requests
trdarr Dec 4, 2013
cad4ccc
Rename duplicate test
trdarr Dec 4, 2013
c72aeca
Add nose and coverage
trdarr Dec 4, 2013
efafbe4
Move api_url to module
trdarr Dec 4, 2013
970d8ec
Add AuthenticationStatusTests
trdarr Dec 4, 2013
fdd5a7c
Add PairingStatusTests
trdarr Dec 4, 2013
373d768
Remove unnecessary (?) status dicts
trdarr Dec 4, 2013
aebfc78
Add ZeroStorageTests
trdarr Dec 4, 2013
e2870b0
Move zero-storage tests
trdarr Dec 4, 2013
7a86014
Improve test coverage (100%)
trdarr Dec 4, 2013
48eda4e
Use requirements.txt
trdarr Dec 4, 2013
a405add
Use variables instead of strings
trdarr Dec 4, 2013
de08095
disable ≠ enable
trdarr Dec 4, 2013
a567701
True ≠ False and vice versa
trdarr Dec 4, 2013
7ce93b5
Merge pull request #18 from toopher/fix-readme-verbiage
trdarr Dec 4, 2013
21fffe9
Sync with develop
trdarr Dec 4, 2013
1d0366d
Sync with develop
trdarr Dec 4, 2013
2beb89f
Update setup.py dependencies
trdarr Dec 10, 2013
7e064fe
Merge pull request #17 from toopher/feature/zero-storage
smholloway Dec 10, 2013
0751d45
Verify SSL, actually
trdarr Dec 19, 2013
38ee09f
Merge pull request #20 from toopher/bugfix/verify-ssl
trdarr Dec 19, 2013
3a6dad2
README updates for zero-storage
smholloway Dec 19, 2013
4f2d5c1
Convert to a lowercase string comparison
smholloway Dec 19, 2013
a6a8991
Update the error message to match the server response
smholloway Dec 19, 2013
4e7cbd1
Use ye olde '%' formatting
trdarr Jan 15, 2014
efbf4c2
Test against Python 2.5 (instead of 2.7)
trdarr Jan 15, 2014
5ba23ad
Test against Python 2.6 instead of 2.5
trdarr Jan 15, 2014
353105f
Use 2.6-compatible unittest.assertRaises calls
trdarr Jan 15, 2014
18425c4
Use assertTrue instead of assertGreaterEqual
trdarr Jan 15, 2014
1c1955c
Merge pull request #22 from toopher/feature/percent-format
trdarr Jan 15, 2014
5705308
Add QR pairing creation method
trdarr Nov 12, 2013
64ef611
Add tests
trdarr Dec 4, 2013
e05b231
Fix __getattr__ implementation to raise correct exception
egrim Jan 22, 2014
0a27402
Merge pull request #23 from toopher/bug/getattr-breach-of-contract
egrim Jan 22, 2014
128d8ad
Fix the tests that verified the breach-of-contract for __getattr__
egrim Jan 22, 2014
f091fac
Merge pull request #24 from toopher/bug/getattr-breach-of-contract
egrim Jan 22, 2014
839b9cf
Fix infinite recursion when unpickling PairingStatus and Authenticati…
egrim Jan 23, 2014
7e9dacd
Merge pull request #25 from toopher/bug/getattr-breach-of-contract
egrim Jan 23, 2014
1b93ff2
Add iframe libs
Mar 24, 2014
a883484
PR feedback - make return type consistent with method name
Mar 25, 2014
b2c36c9
PR feedback
Mar 25, 2014
045b0b5
PR Feedback - this is why we have code review, people
Mar 25, 2014
d385098
update to reflect new pairing deactivated error code 707
Mar 25, 2014
427b8e8
Add iframe README
Mar 25, 2014
1a9b46d
Deal with params passed in as dict-of-lists
Mar 31, 2014
02017b4
Add tests for exceptions
trdarr Apr 1, 2014
b2175b1
Merge pull request #27 from toopher/feature/iframe
dshafer Apr 1, 2014
6d9f347
Require coveralls
trdarr Apr 3, 2014
c6a7db1
Travis? Travis!
trdarr Apr 3, 2014
4441042
Ignore non-Toopher files
trdarr Apr 3, 2014
47f2c76
Change Java comment to Python comment
smholloway Apr 14, 2014
ba18ef4
Convert to fenced code blocks w language specified
smholloway Apr 14, 2014
0faa99b
Fix namespace of errors
smholloway Apr 14, 2014
4a288ec
Convert from Java error to Python error
smholloway Apr 14, 2014
804c236
Test validate with immutable dict
smholloway Apr 14, 2014
ad1e99e
Create a mutable copy of data to satisfy test
smholloway Apr 14, 2014
137b8ae
Remove debug breakpoint
smholloway Apr 14, 2014
9bbf814
Ignore the tests file
smholloway May 18, 2014
34eae22
Switch to using the new web/authenticate endpoint
smholloway May 19, 2014
19e5280
Merge pull request #30 from toopher/issue/29-make-data-mutable
smholloway May 20, 2014
8690572
Merge develop
smholloway May 20, 2014
7f66d9b
Merge pull request #26 from toopher/feature/qr
smholloway May 20, 2014
22aac94
Merge develop
smholloway May 20, 2014
ee53902
Merge pull request #28 from toopher/feature/coveralls
smholloway May 20, 2014
43ee500
Merge pull request #31 from toopher/feature/default-to-all-in-one-ifr…
smholloway May 21, 2014
c72e48c
add helper method for manage_user iframe endpoint
Sep 22, 2014
ff8b272
Remove spaces in default parameter
smholloway Sep 22, 2014
fd77bac
Merge pull request #34 from toopher/feature/manage_user_iframe_endpoint
dshafer Sep 22, 2014
3e5664a
modify tests for allow_inline_pairing parameter in iframe
Sep 25, 2014
581de0e
add allow_inline_pairing kwarg to authenticate iframe
Sep 25, 2014
dc70c64
Merge pull request #35 from toopher/feature/manage_user_iframe_endpoint
dshafer Sep 25, 2014
0ce408b
Add CONTRIBUTING.md
Nov 21, 2014
042315d
Add CONTRIBUTING.md
Nov 21, 2014
c343402
Sync changes with branch 'add-contributing-readme'
Nov 21, 2014
54cc1cb
Add Python version
Nov 21, 2014
8ceb8dc
Merge pull request #36 from toopher/add-contributing-readme
smholloway Dec 24, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[report]
omit =
*/python?.?/*
*/site-packages/nose/*
tests.py
12 changes: 6 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
language: python
python:
- "2.7"
# command to install dependencies
- 2.6
install:
# - pip install -r requirements.txt --use-mirrors
- pip install toopher
# command to run tests
script: python tests.py
- pip install -r requirements.txt --use-mirrors
script:
nosetests tests.py --with-coverage --cover-package=toopher
after_success:
coveralls
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ToopherAPI Python Client

#### Python Version
>=2.6.0

#### Installing Dependencies
Toopher uses [pip](https://pypi.python.org/pypi/pip) to install packages.

To ensure all dependencies are up-to-date run:
```shell
$ pip install -r requirements.txt
```

#### Tests
To run tests enter:
```shell
$ python tests.py
```
119 changes: 119 additions & 0 deletions README-Iframe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Authenticating using the Toopher `<iframe>`
===========================================
Toopher's `<iframe>`-based authentication flow is the simplest way for web developers to integrate Toopher Two-Factor Authentication into an application.

## Toopher `<iframe>` Authentication Overview

The `iframe`-based authentication flow works by inserting an `<iframe>` element into the HTML displayed to the user after a successful username/password validation (but before they are actually logged-in to the service). The iframe URL is generated by our library and its content is served from the Toopher API server. The iframe guides the user through the process of authenticating with Toopher. Once complete, the iframe will return the result of the authentication to your server by `POST`ing the response via HTML to an endpoint of your choice. Your server validates the cryptographic signature using our library which determines whether or not the user successfully authenticated.

Two distinct iframe flows are required:

* the *Pairing* request is used to pair a user account with a particular mobile device (this is typically a one-time process)
* the *Authentication* request to authenticate a particular action on behalf of a user

## Authentication Workflow
### Primary Authentication
We recommend using some form of primary authentication before initiating a Toopher authentication request. Typical primary authentication methods involve verifying that the user has a valid username and password to access the resource being protected.

### Step 1: Embed a request in an `<iframe>`
After verifying the user's primary authentication, but before Assuming the user's primary authentication checks out, the next step is to kickoff Toopher authentication.

1. Generate a URI by specifying the request parameters to the library as detailed below
1. Display a webpage to your user that embeds this URI within an iframe element. The markup requirements for the iframe element are described in the "HTML Markup" section

### Step 2: Validate the result
1. Toopher-iframe results posted back to server
1. Server calls `ToopherIframe.validate()` to verify that result is valid. `.validate()` returns a `Map` of trusted data if the signature is valid, or throws a `SignatureValidationError` if the signature is invalid.
1. The server should check for possible errors returned by the API in the `error_code` map entry
1. If no errors were returned, the result of the authentication is in the `granted` map entry

### HTML markup
The `<iframe>` element must have an id of `toopher_iframe`.

Pages that include the Toopher Authentication or Pairing iframe must include the accompanying javascript library `toopher-web.js` (located in `/assets/js/` in this repository). Toopher's Authentication and Pairing `<iframe>` content is designed for a minimum size of 400x300 px. In the example below, `{{IFRAME_REQUEST_URL}}` is the Authentication or Pairing URL generated by the ToopherIframe library. `{{POSTBACK_URL}}` is the path on your server where the Toopher-Iframe will submit the result of the authentication when it is finished.

```html
<!-- toopher-web.js requires jQuery. uncomment the following line to source it from CDNJS if it is not already included in your page -->
<!-- <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> -->
<script src="/js/toopher-web.js"></script>
<iframe id="toopher_iframe" src="{{IFRAME_REQUEST_URL}}" toopher_postback="{{POSTBACK_URL}}" style="display:inline-block; height:300px; width:100%;"></iframe>
```

There is no difference in the markup required for a Pairing vs. an Authentication iframe request (the generated URI embeds all relevant information).

# Examples

#### Generating an Authentication iframe URI
Every Toopher Authentication session should include a unique `request_token` - a randomized string that is included in the signed request to the Toopher API and returned in the signed response from the Toopher `<iframe>`. To guard against potential replay attacks, your code should validate that the returned `request_token` is the same one used to create the request.

Creating a random request token and storing it in the server-side session using Django:

```python
import random, string
request_token = ''.join(random.choice(string.lowercase + string.digits) for i in range(15))
request.session['ToopherRequestToken'] = request_token
```

The Toopher Authentication API provides the requester a rich set of controls over authentication parameters.

```python
auth_iframe_url = iframe_api.auth_uri(username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl);
```

For the simple case of authenticating a user at login, a `login_uri` helper method is available:

```python
login_iframe_url = iframe_api.login_uri(username, reset_email, request_token)
```

#### Generating a Pairing iframe URI

```python
pair_iframe_url = iframe_api.pair_uri(username, reset_email)
```

#### Validating postback data from Authentication iframe and parsing API errors
In this example, `data` is a `dict` of the form data POSTed to your server from the Toopher Authentication iframe. You should replace the commented blocks with code appropriate for the condition described in the comment.

```python

request_token = request.session.get('ToopherRequestToken')
# invalidate the Request Token to guard against replay attacks
if 'ToopherRequestToken' in request.session:
del request.session['ToopherRequestToken']

try:
validated_data = iframe_api.validate(data, request_token)
if 'error_code' in validated_data:
error_code = validated_data['error_code']
# check for API errors

if error_code == toopher.ERROR_CODE_PAIRING_DEACTIVATED:
# User deleted the pairing on their mobile device.
#
# Your server should display a Toopher Pairing iframe so their account can be re-paired
#
elif error_code == toopher.ERROR_CODE_USER_DISABLED:
# User has been marked as "Opt-Out" in the Toopher API
#
# If your service allows opt-out, the user should be granted access.
#
elif error_code == toopher.ERROR_CODE_USER_UNKNOWN:
# User has never authenticated with Toopher on this server
#
# Your server should display a Toopher Pairing iframe so their account can be paired
#
else:
# signature is valid, and no api errors. check authentication result
auth_pending = validated_data.get('pending').lower() == 'true'
auth_granted = validated_data.get('granted').lower() == 'true'

# authentication_result is the ultimate result of Toopher second-factor authentication
authentication_result = auth_granted and not auth_pending
except toopher.SignatureValidationError, e:
# signature was invalid. User should not authenticated
#
# e.message will return more information about what specifically
# went wrong (incorrect session token, expired TTL, invalid signature)
#
```
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#ToopherPython

[![Build Status](https://travis-ci.org/toopher/toopher-python.png?branch=master)](https://travis-ci.org/toopher/toopher-python)

#### Introduction
ToopherPython is a Toopher API library that simplifies the task of interfacing with the Toopher API from Python code. This project wrangles all the required OAuth and JSON functionality so you can focus on just using the API.

#### Learn the Toopher API
Make sure you visit (http://dev.toopher.com) to get acquainted with the Toopher API fundamentals. The documentation there will tell you the details about the operations this API wrapper library provides.
Make sure you visit [https://dev.toopher.com](https://dev.toopher.com) to get acquainted with the Toopher API fundamentals. The documentation there will tell you the details about the operations this API wrapper library provides.

#### OAuth Authentication
First off, to access the Toopher API you'll need to sign up for an account at the developers portal (http://dev.toopher.com) and create a "requester". When that process is complete, your requester is issued OAuth 1.0a credentials in the form of a consumer key and secret. Your key is used to identify your requester when Toopher interacts with your customers, and the secret is used to sign each request so that we know it is generated by you. This library properly formats each request with your credentials automatically.
First off, to access the Toopher API you'll need to sign up for an account at the [Toopher developers portal](https://dev.toopher.com) and create a "requester". When that process is complete, your requester is issued OAuth 1.0a credentials in the form of a consumer key and secret. Your key is used to identify your requester when Toopher interacts with your customers, and the secret is used to sign each request so that we know it is generated by you. This library properly formats each request with your credentials automatically.

#### The Toopher Two-Step
Interacting with the Toopher web service involves two steps: pairing, and authenticating.
Expand Down Expand Up @@ -35,15 +37,46 @@ auth = api.authenticate(pairing_status.id, "my computer")

# Once they've responded you can then check the status
auth_status = api.get_authentication_status(auth.id)
if (auth_status.pending == false and auth_status.granted == true):
if (auth_status.pending == False and auth_status.granted == True):
# Success!
```

#### Handling Errors
If any request runs into an error a `ToopherApiError` will be thrown with more details on what went wrong.

#### Zero-Storage usage option
Requesters can choose to integrate the Toopher API in a way does not require storing any per-user data such as Pairing ID and Terminal ID - all of the storage
is handled by the Toopher API Web Service, allowing your local database to remain unchanged. If the Toopher API needs more data, it will `raise()` a specific
error that allows your code to respond appropriately.

```python
try:
# optimistically try to authenticate against Toopher API with username and a Terminal Identifier
# Terminal Identifer is typically a randomly generated secure browser cookie. It does not
# need to be human-readable
auth = api.authenticate_by_user_name(user_name, requester_terminal_id)

# if you got here, everything is good! poll the auth request status as described above
# there are four distinct errors ToopherAPI can return if it needs more data
except UserDisabledError:
# you have marked this user as disabled in the Toopher API.
except UserUnknownError:
# This user has not yet paired a mobile device with their account. Pair them
# using api.pair() as described above, then re-try authentication
except TerminalUnknownError:
# This user has not assigned a "Friendly Name" to this terminal identifier.
# Prompt them to enter a terminal name, then submit that "friendly name" to
# the Toopher API:
# api.create_user_terminal(user_name, terminal_name, requester_terminal_id)
# Afterwards, re-try authentication
except PairingDeactivatedError:
# this user does not have an active pairing,
# typically because they deleted the pairing. You can prompt
# the user to re-pair with a new mobile device.
```

#### Dependencies
This library uses the python-oauth2 library to handle OAuth signing and httplib2 to make the web requests. If you install using pip (or easy_install) they'll be installed automatically for you.
This library uses the [Requests](http://docs.python-requests.org/en/latest/) library to handle OAuth signing and to make the web requests. If you install using pip (or easy_install) they'll be installed automatically for you.

#### Try it out
Check out `demo.py` for an example program that walks you through the whole process! Just download the contents of this repo, make sure you have the dependencies listed above installed, and then run it like-a-this:
Expand Down
6 changes: 3 additions & 3 deletions demo_bells_and_whistles.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ def print_sep(char='-'):
pair_type = 'pairing phrase'

print pair_help
pairing_key = raw_input('Enter {0}: '.format(pair_type))
pairing_key = raw_input('Enter %s: ' % pair_type)
while not pairing_key:
print 'Please enter a {0} to continue'.format(pair_type)
pairing_key = raw_input('Enter {0}: '.format(pair_type))
print 'Please enter a %s to continue' % pair_type
pairing_key = raw_input('Enter %s: ' % pair_type)

user_name = raw_input('Enter a username for this pairing [%s]: ' % DEFAULT_USERNAME)
if not user_name:
Expand Down
11 changes: 11 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
PyYAML==3.11
coverage==3.7
coveralls==0.4.1
docopt==0.6.1
httpretty==0.7.0
nose==1.3.0
oauthlib==0.6.0
requests==2.0.1
requests-oauthlib==0.4.0
urllib3==1.7.1
Werkzeug==0.9.4
9 changes: 3 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

setup(
name='toopher',
version='1.0.5',
version='1.1.0',
author='Toopher, Inc.',
author_email='support@toopher.com',
author_email='dev@toopher.com',
url='https://dev.toopher.com',
description='Wrapper library for the Toopher authentication API',
classifiers=[
Expand All @@ -18,9 +18,6 @@
'Topic :: Software Development :: Libraries :: Python Modules',
],
packages=['toopher'],
package_data = {'toopher': ['toopher.pem']},
test_suite='tests',
install_requires=[
'oauth2',
]
install_requires=['requests-oauthlib>=0.4.0']
)
Loading