Home

Awesome

<img src="https://raw.github.com/snarfed/oauth-dropins/main/oauth_dropins/static/oauth_shiny.png" alt="OAuth logo" width="125" /> oauth-dropins Circle CI

Drop-in Python OAuth for popular sites!

About

This is a collection of drop-in Python Flask views for the initial OAuth client flows for many popular sites, including Blogger, Disqus, Dropbox, Facebook, Flickr, GitHub, Google, IndieAuth, Instagram, LinkedIn, Mastodon, Medium, Tumblr, Twitter, and WordPress.com.

oauth-dropins stores user credentials in Google Cloud Datastore. It's primarily designed for Google App Engine, but it can be used in any Python web application, regardless of host or framework.

This software is released into the public domain. See LICENSE for details.

Quick start

Here's a full example of using the GitHub drop-in.

  1. Install oauth-dropins with pip install oauth-dropins.

  2. Put your GitHub OAuth application's ID and secret in two plain text files in your app's root directory, github_client_id and github_client_secret. (If you use git, you'll probably also want to add them to your .gitignore.)

  3. Create a github_oauth.py file with these contents:

    from oauth_dropins import github
    from app import app  # ...or wherever your Flask app is
    
    app.add_url_rule('/start',
                     view_func=github.Start.as_view('start', '/callback'),
                     methods=['POST'])
    app.add_url_rule('/callback',
                     view_func=github.Callback.as_view('callback', '/after'))
    

Voila! Send your users to /github/start when you want them to connect their GitHub account to your app, and when they're done, they'll be redirected to /after?access_token=... in your app.

All of the sites provide the same API. To use a different one, just import the site module you want and follow the same steps. The filenames for app keys and secrets also differ by site; see each site's .py file for its filenames.

Usage details

There are three main parts to an OAuth drop-in: the initial redirect to the site itself, the redirect back to your app after the user approves or declines the request, and the datastore entity that stores the user's OAuth credentials and helps you use them. These are implemented by Start and Callback, which are Flask View classes, and auth entities, which are Google Cloud Datastore ndb models.

Start

This view class redirects you to an OAuth-enabled site so it can ask the user to grant your app permission. It has two useful methods:

import flask

class MyView(Start):
  def dispatch_request(self):
    ...
    flask.redirect(self.redirect_url())

Callback

This class handles the HTTP redirect back to your app after the user has granted or declined permission. It also has two useful methods:

class MyCallback(github.Callback):
  def finish(self, auth_entity, state=None):
    super().finish(auth_entity, state=state)  # ignore returned redirect
    self.response.write('Hi %s, thanks for connecting your %s account.' %
        (auth_entity.user_display_name(), auth_entity.site_name()))

Auth entities

Each site defines a Google Cloud Datastore ndb.Model class that stores each user's OAuth credentials and other useful information, like their name and profile URL. The class name is generally of the form <em>Site</em>Auth, e.g. GitHubAuth. Here are the useful methods:

The following methods are optional. Auth entity classes usually implement at least one of them, but not all.

Troubleshooting/FAQ

  1. If you get this error:

    bash: ./bin/easy_install: ...bad interpreter: No such file or directory
    

You've probably hit this virtualenv bug: virtualenv doesn't support paths with spaces.

The easy fix is to recreate the virtualenv in a path without spaces. If you can't do that, then after creating the virtualenv, but before activating it, edit the activate, easy_install and pip files in local/bin/ to escape any spaces in the path.

For example, in activate, VIRTUAL_ENV=".../has space/local" becomes VIRTUAL_ENV=".../has\ space/local", and in pip and easy_install the first line changes from #!".../has space/local/bin/python" to #!".../has\ space/local/bin/python".

This should get virtualenv to install in the right place. If you do this wrong at first, you'll have installs in eg /usr/local/lib/python3.7/site-packages that you need to delete, since they'll prevent virtualenv from installing into the local site-packages.

  1. If you see errors importing or using tweepy, it may be because six.py isn't installed. Try pip install six manually. tweepy does include six in its dependencies, so this shouldn't be necessary. Please let us know if it happens to you so we can debug!

  2. If you get an error like this:

    Running setup.py develop for gdata
    ...
    error: option --home not recognized
    ...
    InstallationError: Command /usr/bin/python -c "import setuptools, tokenize; __file__='/home/singpolyma/src/bridgy/src/gdata/setup.py'; exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" develop --no-deps --home=/tmp/tmprBISz_ failed with error code 1 in .../src/gdata
    

...you may be hitting Pip bug 1833. Are you passing -t to pip install? Use the virtualenv instead, it's your friend. If you really want -t, try removing the -e from the lines in requirements.txt that have it.

Changelog

6.5 - unreleased

6.4 - 2024-06-24

Misc webutil updaates.

6.3 - 2024-03-15

Miscellaneous changes in webutil.

6.2 - 2023-09-15

Miscellaneous changes in webutil.

6.1 - 2023-03-22

Non-breaking changes:

6.0 - 2022-12-03

Breaking changes:

Non-breaking changes:

5.0 - 2022-03-23

Breaking changes:

Non-breaking changes:

4.0 - 2021-09-15

Breaking changes:

Non-breaking changes:

3.1 - 2021-04-03

3.0 - 2020-03-14

Breaking changes:

Non-breaking changes:

2.2 - 2019-11-01

2.0 - 2019-02-25

1.14 - 2018-11-12

1.13 - 2018-08-08

1.12 - 2018-03-24

1.11 - 2018-03-08

1.10 - 2017-12-10

Mostly just internal changes to webutil to support granary v1.10.

1.9 - 2017-10-24

Mostly just internal changes to webutil to support granary v1.9.

1.8 - 2017-08-29

1.7 - 2017-02-27

1.6 - 2016-11-21

1.5 - 2016-08-25

1.4 - 2016-06-27

1.3 - 2016-04-07

1.2 - 2016-01-11

1.1 - 2015-09-06

1.0 - 2015-06-27

Development

Pull requests are welcome! Feel free to ping me in #indieweb-dev with any questions.

First, fork and clone this repo. Then, install the Google Cloud SDK and run gcloud components install cloud-firestore-emulator to install the Firestore emulator. Once you have them, set up your environment by running these commands in the repo root directory:

gcloud config set project oauth-dropins
git submodule init
git submodule update
python3 -m venv local
source local/bin/activate
pip install -r requirements.txt

Run the demo app locally with flask run:

gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /dev/null &
GAE_ENV=localdev FLASK_ENV=development flask run -p 8080

To deploy to production:

gcloud -q beta app deploy --no-cache oauth-dropins *.yaml

The docs are built with Sphinx, including apidoc, autodoc, and napoleon. Configuration is in docs/conf.py To build them, first install Sphinx with pip install sphinx. (You may want to do this outside your virtualenv; if so, you'll need to reconfigure it to see system packages with python3 -m venv --system-site-packages local.) Then, run docs/build.sh.

Release instructions

Here's how to package, test, and ship a new release. (Note that this is largely duplicated in granary's readme too.)

  1. Run the unit tests.
    source local/bin/activate.csh
    gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /dev/null &
    sleep 2s
    python -m unittest discover
    kill %1
    deactivate
    
  2. Bump the version number in setup.py and docs/conf.py. git grep the old version number to make sure it only appears in the changelog. Change the current changelog entry in README.md for this new version from unreleased to the current date.
  3. Build the docs. If you added any new modules, add them to the appropriate file(s) in docs/source/. Then run ./docs/build.sh.
  4. git commit -am 'release vX.Y'
  5. Upload to test.pypi.org for testing.
    python setup.py clean build sdist
    setenv ver X.Y
    source local/bin/activate.csh
    twine upload -r pypitest dist/oauth-dropins-$ver.tar.gz
    
  6. Install from test.pypi.org.
    cd /tmp
    python -m venv local
    source local/bin/activate.csh
    pip install --upgrade pip
    # mf2py 1.1.2 on test.pypi.org is broken :(
    pip install mf2py
    pip install -i https://test.pypi.org/simple --extra-index-url https://pypi.org/simple oauth-dropins
    deactivate
    
  7. Smoke test that the code trivially loads and runs.
    source local/bin/activate.csh
    python
    # run test code below
    deactivate
    
    Test code to paste into the interpreter:
    from oauth_dropins.webutil import util
    util.__file__
    util.UrlCanonicalizer()('http://asdf.com')
    # should print 'https://asdf.com/'
    exit()
    
  8. Tag the release in git. In the tag message editor, delete the generated comments at bottom, leave the first line blank (to omit the release "title" in github), put ### Notable changes on the second line, then copy and paste this version's changelog contents below it.
    git tag -a v$ver --cleanup=verbatim
    git push
    git push --tags
    
  9. Click here to draft a new release on GitHub. Enter vX.Y in the Tag version box. Leave Release title empty. Copy ### Notable changes and the changelog contents into the description text box.
  10. Upload to pypi.org!
    twine upload dist/oauth-dropins-$ver.tar.gz
    

Related work