
Security News
Another Round of TEA Protocol Spam Floods npm, But It’s Not a Worm
Recent coverage mislabels the latest TEA protocol spam as a worm. Here’s what’s actually happening.
oauth-dropins
Advanced tools
oauth-dropins Drop-in Python OAuth for popular sites!
This is a collection of drop-in Python Flask views for the initial OAuth client flows for many popular sites, including Blogger, Bluesky, 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.
pip install oauth-dropins.oauth-dropins is dedicated to the public domain. See LICENSE for details.
Here's a full example of using the GitHub drop-in.
Install oauth-dropins with pip install oauth-dropins.
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.)
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.
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.
StartThis 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:
The constructor, __init__(self, to_path, scopes=None). to_path is the OAuth callback, ie URL path on your site that the site's OAuth flow should redirect back to after it's done. This is handled by a Callback view in your application, which needs to handle the to_path route.
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 supported in most sites, but not all.
Some OAuth 1 sites support alternatives to scopes. For Twitter, the Start constructor takes an additional access_type kwarg that may be read or write. It's passed through to Twitter x_auth_access_type. For Flickr, Start 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 site to initiate the OAuth flow. Start will redirect here automatically if it's used in a WSGI application, but you can call this manually if you want to control that redirect yourself:
import flask
class MyView(Start):
def dispatch_request(self):
...
flask.redirect(self.redirect_url())
CallbackThis class handles the HTTP redirect back to your app after the user has granted or declined permission. It also has two useful methods:
The constructor, __init__(self, to_path, scopes=None). to_path is the URL path on your site that users should be redirected to after the callback view is done. It will include a state query parameter with the value provided to Start. 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 to_path, but you can subclass Callback and override it to run your own code instead of redirecting:
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()))
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 SiteAuth, e.g. GitHubAuth. 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 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().
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.
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!
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.
created and updated properties to auth models.redirect_uri kwarg to oauth_client_for_pds, use in OAuthStart and OAuthCallback.OAuthStart.redirect_url: normalize localhost to 127.0.0.1.Start: accept handles in instance URLs, eg https://snarfed@mas.to.actor_id bug fix.$type: app.bsky.actor.defs#profileViewDetailed to BlueskyAuth.user_json.BlueskyAuth.oauth_api method.read:accounts to profile.MastodonAuth.actor_id method.PixelfedAuth.actor_id method.auth arg to logout to only log out a single account.get_logins and logout functions.pds_url attribute to BlueskyAuth.BaseAuth.image_url method.Link headers. (Thanks catgirlinspace!)Misc webutil updaates.
@ (bridgy#1667).Miscellaneous changes in webutil.
Miscellaneous changes in webutil.
Non-breaking changes:
IndieAuth datastore entities.Breaking changes:
webutil.handlers, which was based on the largely unmaintained webapp2. All known clients have migrated to Flask and webutil.flask_util.Non-breaking changes:
twitter_v2 module for Twitter's new OAuth 2 with PKCE support and v2 API.declined=True, which is still wrong, but less bad.MastodonAuth.access_token_str from ndb TextProperty to StringProperty so that it's indexed in the Datastore.state parameter, return HTTP 400 instead of raising JSONDecodeError.Breaking changes:
Non-breaking changes:
flask run for local development.webutil.util.set_user_agent to set User-Agent header to be sent with all HTTP requests.Breaking changes:
Migrate from webapp2 to Flask. webapp2 had a good run, but it's no longer actively developed, and Flask is one of the most widely adopted standalone web frameworks in the Python community.
Remove to() class methods. Instead, now pass redirect paths to Flask's as_view() function, eg:
app = Flask()
app.add_url_rule('/start', view_func=twitter.Callback.as_view('start', '/oauth_callback'))
Remove deprecated blogger_v2 module alias.
webutil: migrate webapp2 HTTP request handlers in the handlers module - XrdOrJrdHandler, HostMetaHandler, and HostMetaXrdsHandler - to Flask views in a new flask_util module.
Non-breaking changes:
webutil: implement Webmention protocol in new webmention module.webutil: add misc Flask utilities and helpers in new flask_util module.Breaking changes:
handlers.memcache_response(), which used Python 2 App Engine's memcache service, with cache_response(), which uses local runtime memory.handlers.TemplateHandler.USE_APPENGINE_WEBAPP toggle to use Python 2 App Engine's google.appengine.ext.webapp2.template instead of Jinja.api_from_creds(), creds(), and http() methods have been removed. Use the remaining api() method to get a BloggerClient, or access_token() to make API calls manually.GoogleAuth with the new GoogleUser NDB model class, which doesn't depend on the deprecated oauth2client.http() method (which returned an httplib2.Http).StartHandler: drop APP_NAME/APP_URL class attributes and app_name/app_url kwargs in the to() method and replace them with new app_name()/app_url() methods that subclasses should override, since they often depend on WSGI environment variables like HTTP_HOST and SERVER_NAME that are available during requests but not at runtime startup.webutil:
handlers.memcache_response() since the Python 3 runtime doesn't include memcache.handlers.TemplateHandler support for webapp2.template via USE_APPENGINE_WEBAPP, since the Python 3 runtime doesn't include webapp2 built in.cache and fail_cache_time_secs kwargs from util.follow_redirects(). Caching is now built in. You can bypass the cache with follow_redirects.__wrapped__(). Details.Non-breaking changes:
state query parameter now works!outer_classes kwarg to button_html() for the outer <div>, eg as Bootstrap columns.image_file kwarg to StartHandler.button_html()button_html() method to all StartHandler classes. Generates the same button HTML and styling as on oauth-dropins.appspot.com.blogger_v2 to blogger. The blogger_v2 module name is still available as an alias, implemented via symlink, but is now deprecated.name field.json module to ujson (built into App Engine) to speed up JSON parsing and encoding.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.start_time is before 2008-04-01 (App Engine's rough launch window).app-engine-python 1.9.76 and onward. Background.google-api-python-client from 1.6.3 to 1.7.4 to stop using the global HTTP Batch endpoint.state to the initial OAuth endpoint directly, instead of encoding it into the redirect URL, so the redirect can match the Strict Mode whitelist.Mostly just internal changes to webutil to support granary v1.10.
Mostly just internal changes to webutil to support granary v1.9.
FlickrAuth.urlopen() method.me parameter, which is going away.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.
Here's how to package, test, and ship a new release. (Note that this is largely duplicated in granary's readme too.)
git checkout main
git pull
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
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.docs/source/. Then run ./docs/build.sh.git commit -am 'release vX.Y'python setup.py clean build sdist
setenv ver X.Y
twine upload -r pypitest dist/oauth_dropins-$ver.tar.gz
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
python
# run test code below
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()
### 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
vX.Y in the Tag version box. Leave Release title empty. Copy ### Notable changes and the changelog contents into the description text box.twine upload dist/oauth_dropins-$ver.tar.gz
FAQs
Drop-in OAuth Flask views for many popular sites.
We found that oauth-dropins demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Recent coverage mislabels the latest TEA protocol spam as a worm. Here’s what’s actually happening.

Security News
PyPI adds Trusted Publishing support for GitLab Self-Managed as adoption reaches 25% of uploads

Research
/Security News
A malicious Chrome extension posing as an Ethereum wallet steals seed phrases by encoding them into Sui transactions, enabling full wallet takeover.