Skip to main content

Drop-in App Engine OAuth client handlers for many popular sites.

Project description

OAuth logo oauth-dropins Circle CI

Drop-in OAuth for Python App Engine!


This is a collection of drop-in Google App Engine Python request handlers for the initial OAuth client flows for many popular sites, including Blogger, Disqus, Dropbox, Facebook, Flickr, GitHub, Google, IndieAuth, Instagram, Medium, Tumblr, Twitter, and

Requires either the App Engine Python SDK or the Google Cloud SDK (aka gcloud) with the gcloud-appengine-python and gcloud-appengine-python-extras components. All other dependencies are handled by pip and enumerated in requirements.txt. We recommend that you install with pip in a virtualenv. App Engine details here.

If you clone the repo directly or want to contribute, see Development for setup instructions.

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

Quick start

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

  1. Make sure you have either the App Engine Python SDK version 1.9.15 or later (for vendor support) or the Google Cloud SDK (aka gcloud) installed and on your $PYTHONPATH, e.g. export PYTHONPATH=$PYTHONPATH:/usr/local/google_appengine. oauth-dropins's file needs it during installation.

  2. Install oauth-dropins into a virtualenv somewhere your App Engine project's directory, e.g. local/:

    source local/bin/activate
    pip install oauth-dropins
  3. Add this to the file in your project's root directory (background):

    from google.appengine.ext import vendor
    from oauth_dropins.appengine_config import *
  4. Put your Facebook application's ID and secret in two plain text files in your app's root directory, facebook_app_id and facebook_app_secret. (If you use git, you'll probably also want to add them to your .gitignore.)

  5. Create a file with these contents:

    from oauth_dropins import facebook
    import webapp2
    application = webapp2.WSGIApplication([
  6. Add these lines to app.yaml:

    - url: /facebook/(start_oauth|oauth_callback)
      script: facebook_oauth.application
      secure: always

Voila! Send your users to /facebook/start_oauth when you want them to connect their Facebook account to your app, and when they're done, they'll be redirected to /next?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; has the full list.

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 StartHandler, CallbackHandler, and auth entities, respectively.

The request handlers are full WSGI applications and may be used in any Python web framework that supports WSGI (PEP 333). Internally, they're implemented with webapp2.


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

  • to(callback_path, scopes=None) is a factory method that returns a request handler class you can use in a WSGI application. The argument should be the path mapped to CallbackHandler in your application. This also usually needs to match the callback URL in your app's configuration on the destination site.

    If you want to add OAuth scopes beyond the default one(s) needed for login, you can pass them to the scopes kwarg as a string or sequence of strings, or include them in the scopes query parameter in the POST request body. This is currently supported with Facebook, Google, Blogger, and Instagram.

    Some of the sites that use OAuth 1 support alternatives. For Twitter, takes an additional access_type kwarg that may be read or write. It's passed through to Twitter x_auth_access_type. For Flickr, the start handler accepts a perms POST query parameter that may be read, write or delete; it's passed through to Flickr unchanged. (Flickr claims it's optional, but sometimes breaks if it's not provided.)

  • redirect_url(state=None) returns the URL to redirect to at the destination site to initiate the OAuth flow. StartHandler will redirect here automatically if it's used in a WSGI application, but you can also instantiate it and call this manually if you want to control that redirect yourself:

class MyHandler(webapp2.RequestHandler):
  def get(self):
    handler_cls ='/facebook/oauth_callback')
    handler = handler_cls(self.request, self.response)

However, this is not currently supported for Google and Blogger. Hopefully that will be fixed in the future.


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

  • to(callback_path) is a factory method that returns a request handler class you can use in a WSGI application, similar to StartHandler. The callback path is the path in your app that users should be redirected to after the OAuth flow is complete. It will include a state query parameter with the value provided by the StartHandler. It will also include an OAuth token in its query parameters, either access_token for OAuth 2.0 or access_token_key and access_token_secret for OAuth 1.1. It will also include an auth_entity query parameter with the string key of an auth entity that has more data (and functionality) for the authenticated user. If the user declined the OAuth authorization request, the only query parameter besides state will be declined=true.

  • finish(auth_entity, state=None) is run in the initial callback request after the OAuth response has been processed. auth_entity is the newly created auth entity for this connection, or None if the user declined the OAuth authorization request.

    By default, finish redirects to the path you specified in to(), but you can subclass CallbackHandler and override it to run your own code inside the OAuth callback instead of redirecting:

class MyCallbackHandler(facebook.CallbackHandler):
  def finish(self, auth_entity, state=None):
    self.response.write('Hi %s, thanks for connecting your %s account.' %
        (auth_entity.user_display_name(), auth_entity.site_name()))

However, this is not currently supported for Google and Blogger. Hopefully that will be fixed in the future.

Auth entities

Each site defines an App Engine 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 of the form SiteAuth, e.g. FacebookAuth. Here are the useful methods:

  • site_name() returns the human-readable string name of the site, e.g. "Facebook".

  • user_display_name() returns a human-readable string name for the user, e.g. "Ryan Barrett". This is usually their first name, full name, or username.

  • access_token() returns the OAuth access token. For OAuth 2 sites, this is a single string. For OAuth 1.1 sites (currently just Twitter, Tumblr, and Flickr), this is a (string key, string secret) tuple.

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

  • api() returns a site-specific API object. This is usually a third party library dedicated to the site, e.g. Tweepy or python-instagram. See the site class's docstring for details.

  • urlopen(data=None, timeout=None) wraps urllib2.urlopen() and adds the OAuth credentials to the request. Use this for making direct HTTP request to a site's REST API. Some sites may provide get() instead, which wraps requests.get().

  • http() returns an httplib2.Http instance that adds the OAuth credentials to requests.


  1. If you get this error:

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

You've probably hit this open virtualenv bug (fixed but not merged): 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 /usr/local/lib/python2.7/site-packages that you need to delete, since they'll prevent virtualenv from installing into the local site-packages.

  1. If you're using Twitter, and import requests or something similar fails with:

    ImportError: cannot import name certs

    or you see an exception like:

    File ".../site-packages/tweepy/", line 68, in _get_request_token
      raise TweepError(e)
    TweepError: must be _socket.socket, not socket
 need to configure App Engine's SSL. Add this to your app.yaml:

    - name: ssl
      version: latest

If you use dev_appserver, you'll also need to apply this workaround (more background). Annoying, I know.

  1. If you see errors importing or using tweepy, it may be because 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:

      File "oauth_dropins/webutil/test/", line 5, in <module>
        import dev_appserver
    ImportError: No module named dev_appserver
    InstallationError: Command python egg_info failed with error code 1 in /home/singpolyma/src/bridgy/src/oauth-dropins-master either don't have /usr/local/google_appengine in your PYTHONPATH, or you have it as a relative directory. pip requires fully qualified directories.

  1. If you get an error like this:

    Running develop for gdata
    error: option --home not recognized
    InstallationError: Command /usr/bin/python -c "import setuptools, tokenize; __file__='/home/singpolyma/src/bridgy/src/gdata/'; 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 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.freeze.txt that have it.


2.2 - 2019-11-01

  • Add LinkedIn and Mastodon!
  • Add Python 3.7 support, and improve overall Python 3 compatibility.
  • Add new button_html() method to all StartHandler classes. Generates the same button HTML and styling as on
  • Blogger: rename module from blogger_v2 to blogger. The blogger_v2 module name is still available as an alias, implemented via symlink, but is now deprecated.
  • Dropbox: fix crash with unicode header value.
  • Google: fix crash when user object doesn't have name field.
  • Facebook: upgrade Graph API version from 2.10 to 4.0.
  • Update a number of dependencies.
  • Switch from Python's built in json module to ujson (built into App Engine) to speed up JSON parsing and encoding.

2.0 - 2019-02-25

  • Breaking change: switch from Google+ Sign-In (which shuts down in March) to Google Sign-In. Notably, this removes the googleplus module and adds a new google_signin module, renames the GooglePlusAuth class to GoogleAuth, and removes its api() method. Otherwise, the implementation is mostly the same.
  • webutil.logs: return HTTP 400 if start_time is before 2008-04-01 (App Engine's rough launch window).

1.14 - 2018-11-12

1.13 - 2018-08-08

  • IndieAuth: support JSON code verification responses as well as form-encoded (snarfed/bridgy#809).

1.12 - 2018-03-24

  • More Python 3 updates and bug fixes in webutil.util.

1.11 - 2018-03-08

  • Add GitHub!
  • Facebook:
  • Add Python 3 support to webutil.util!
  • Add humanize dependency for webutil.logs.

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.

  • Flickr:
    • Handle punctuation in error messages.

1.8 - 2017-08-29

  • Facebook:
    • Upgrade Graph API from v2.6 to v2.10.
  • Flickr:
    • Fix broken FlickrAuth.urlopen() method.
  • Medium:
    • Bug fix for Medium OAuth callback error handling.
  • IndieAuth:
    • Store authorization endpoint in state instead of rediscovering it from me parameter, which is going away.

1.7 - 2017-02-27

  • Updates to bundled webutil library, notably WideUnicode class.

1.6 - 2016-11-21

1.5 - 2016-08-25

1.4 - 2016-06-27

  • Upgrade Facebook API from v2.2 to v2.6.

1.3 - 2016-04-07

  • Add IndieAuth.
  • More consistent logging of HTTP requests.
  • Set up Coveralls.

1.2 - 2016-01-11

  • Flickr:
    • Add upload method.
    • Improve error handling and logging.
  • Bug fixes and cleanup for constructing scope strings.
  • Add developer setup and troubleshooting docs.
  • Set up CircleCI.

1.1 - 2015-09-06

  • Flickr: split out file.
  • Add a number of utility functions to webutil.

1.0 - 2015-06-27

  • Initial PyPi release.


You'll need the App Engine Python SDK version 1.9.15 or later (for vendor support) or the Google Cloud SDK (aka gcloud) with the gcloud-appengine-python and gcloud-appengine-python-extras components. Add them to your $PYTHONPATH, e.g. export PYTHONPATH=$PYTHONPATH:/usr/local/google_appengine, and then run:

git submodule init
git submodule update
virtualenv local
source local/bin/activate
pip install -r requirements.txt

# We install gdata in source mode, and App Engine doesn't follow .egg-link
# files, so add a symlink to it.
ln -s ../../../src/gdata/src/gdata local/lib/python2.7/site-packages/gdata
ln -s ../../../src/gdata/src/atom local/lib/python2.7/site-packages/atom

python test

Most dependencies are clean, but we've made patches to gdata-python-client below that we haven't (yet) tried to push upstream. If we ever switch its submodule repo for, make sure the patches are included!

To deploy:

python -m unittest discover && git push && gcloud -q app deploy oauth-dropins *.yaml

The docs are built with Sphinx, including apidoc, autodoc, and napoleon. Configuration is in docs/ 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 virtualenv --system-site-packages local.) Then, run docs/

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
    python2 -m unittest discover
    source local3/bin/activate.csh
    python3 -m unittest oauth_dropins.webutil.tests.test_util
  2. Bump the version number in and docs/ git grep the old version number to make sure it only appears in the changelog. Change the current changelog entry in 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/
  4. git commit -am 'release vX.Y'
  5. Upload to for testing.
    python3 clean build sdist
    twine upload -r pypitest dist/oauth-dropins-X.Y.tar.gz
  6. Install from, both Python 2 and 3.
    cd /tmp
    virtualenv local
    source local/bin/activate.csh
    # mf2py 1.1.2 on is broken :(
    pip install mf2py
    pip install -i --extra-index-url oauth-dropins
    python3 -m venv local3
    source local3/bin/activate.csh
    pip3 install --upgrade pip
    # mf2py 1.1.2 on is broken :(
    pip3 install mf2py
    pip3 install -i --extra-index-url oauth-dropins
  7. Smoke test that the code trivially loads and runs, in both Python 2 and 3.
     source local/bin/activate.csh
     # run test code below
    source local3/bin/activate.csh
    # run test code below
    Test code to paste into the interpreter:
    from oauth_dropins.webutil import util
    # should print ''
  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 vX.Y --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!
    twine upload dist/oauth-dropins-X.Y.tar.gz

Related work


  • Google and Blogger need some love:
    • handle declines
    • allow overriding CallbackHandler.finish()
    • support StartHandler.redirect_url()
    • allow more than one CallbackHandler per app
  • clean up app key/secret file handling. (standardize file names? put them in a subdir?)
  • implement CSRF protection for all sites
  • implement Blogger's v3 API

Project details

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for oauth-dropins, version 2.2
Filename, size File type Python version Upload date Hashes
Filename, size oauth-dropins-2.2.tar.gz (1.1 MB) File type Source Python version None Upload date Hashes View

Supported by

AWS AWS Cloud computing Datadog Datadog Monitoring Facebook / Instagram Facebook / Instagram PSF Sponsor Fastly Fastly CDN Google Google Object Storage and Download Analytics Huawei Huawei PSF Sponsor Microsoft Microsoft PSF Sponsor NVIDIA NVIDIA PSF Sponsor Pingdom Pingdom Monitoring Salesforce Salesforce PSF Sponsor Sentry Sentry Error logging StatusPage StatusPage Status page