Heroku Bouncer - Python edition
WSGI middleware that requires Heroku OAuth for all requests.
Inspired and cribbed from heroku-bouncer.
Installation
pip install heroku-bouncer
Usage
-
Create your OAuth client using
/auth/heroku/callback/
as your callback endpoint:heroku clients:create likeaboss https://likeaboss.herokuapp.com/auth/heroku/callback/
-
Set
SECRET_KEY
,HEROKU_OAUTH_ID
andHEROKU_OAUTH_SECRET
in your environment:heroku config:set SECRET_KEY=... heroku config:set HEROKU_OAUTH_ID=... heroku config:set HEROKU_OAUTH_SECRET=...
-
Wire up the middleware. See options for the options you can pass in here:
import heroku_bouncer from your.wsgi.application import app app = heroku_bouncer.bouncer(app)
-
This will require Heroku OAuth for all access to the app. The user (i.e. email address of the Heroku account) will be stored in
"REMOTE_USER"
in the WSGI environ. For more access to the authenticated user, check out the session object (see below).
For more details about the user, you can access "wsgioauth2.session"
in the
WSGI environ. You'll probably be getting this environ from whatever framework
you're using. For example, in Django you'll find this in
request.META['wsgioauth2.session']
; in Flask it'll be
flask.request.environ['wsgioauth2.session']
This is a dict-like object with a couple of useful keys:
-
"user"
- the Heroku account object from the Platform API. -
"username"
- the Heroku account email address (same asenv["REMOTE_USER"]
). -
"access_token"
- the OAuth user access token. You can use this to make authenticated requests against the Heroku API.
Once you've got an authenticated user, you can make OAuth requests on their behalf against the Heroku Platform API. The key is to set two HTTP headers:
-
Set the
Authorization
header to"Bearer: TOKEN"
, whereTOKEN
is the OAuth token found inenv["wsgioauth2.session"]["token"]
. -
Set the
Accept
header toapplication/vnd.heroku+json; version=3
to select the "v3" API.
For details about Heroku API, see the getting started guide and the Platform API reference.
For example, using requests, you could create a new app as the authenticated user using something like this:
headers = {
'Authorization': 'Bearer: %s' % environ['wsgioauth2.session']['token'],
'Accept': 'application/vnd.heroku+json; version=3'
}
requests.post('https://api.heroku.com/apps', headers=headers)
You can pass extra options as keyword arguments to heroku_bouncer.bouncer()
.
Those options are:
-
set_remote_user
- ifTrue
(the default), then the Heroku username (which is also an email address) will end up onenviron["REMOTE_USER"]
. There's not a great reason to set this toFalse
, but you can if you really feel like it I guess. -
auth_callback
- an optional callback function that will be called to decide if the authorized user should be allowed in. This will be passed a single arguent, the session object. For example, to limit access to users from a single domain:
def callback(session):
return session['user']['email'].endswith('@example.com')
app = bouncer(app, auth_callback=callback)
-
scope
- the OAuth scope(s), as defined in Heroku's documentation, that your app requires. If you're requesting more than one scope, the scopes should be separated by spaces (e.g.app = bouncer(app, scope="read write")
). Defaults to"identity"
. -
path
- the path to use for the OAuth callback. This is the same path you'll pass toheroku clients:create
; note that it must end in a trailing slash!. Defaults to/auth/heroku/callback/
, which you can probably leave alone unless that conflcits with a URL in your real app. -
cookie
- name of the cookie to use. Defaults to"herokuoauthsess"
. -
forbidden_path
- What path should be used to display the 403 Forbidden page. Any forbidden user will be redirected to this path and a default 403 Forbidden page will be shown. To override the default page see the next option. -
forbidden_passthrough
- by default a generic 403 page will be generated. Set this toTrue
to pass the request through to the protected application. -
client_id
- the OAuth client ID. Read from `os.environ['HEROKU_OAUTH_ID'] if not passed explicitly. -
client_secret
- the OAuth client secret. Read fromos.environ['HEROKU_OAUTH_SECRET']
if not passed explicitly. -
secret_key
- a secret key used to sign the session. Read from ``os.environ['SECRET_KEY']` if not passed explicitly.
Hooking up with Flask is pretty simple; you'll just set app.wsgi_app
following
the example in the documentation:
import flask
import heroku_bouncer
app = flask.Flask(__name__)
#
# ... your app here ...
#
app.wsgi_app = heroku_bouncer.bouncer(app.wsgi_app)
Integrating with Django's a bit more complex. First, you'll need to
enable authentication against REMOTE_USER.
by adding 'django.contrib.auth.middleware.RemoteUserMiddleware'
to your MIDDLEWARE_CLASSES
- make sure it's after
AuthenticationMiddleware
.
Then, you'll need to create a remote user backend to map Heroku users to your users. At the very least, you'll need to deal with the fact that Heroku uses emails for usernames and Django doesn't. So a minimal remote user backend might look like this:
import hashlib
from django.contrib.auth.backends import RemoteUserBackend
class HerokuRemoteUserBackend(RemoteUserBackend):
create_unknown_user = True
def clean_username(self, username):
return hashlib.md5(username).hexdigest()
In practice, you may want to do something more complex (probably involving a
[custom user object](https://docs.djangoproject.com/en/dev/topics/auth/customizing
/#extending-the-existing-user-model)), including probably overriding
configure_user()
as well to control initial permissions and such. See [the
docs for remote user authentication](https://docs.djangoproject.com/en/dev/howto
/auth-remote-user/#remoteuserbackend) for more details, as well as the more
general documentation on customizing authentication.
Once you've got your remote user backend, you'll need to add it to
AUTHENTICATION_BACKENDS
:
AUTHENTICATION_BACKENDS = ['myproject.auth.HerokuRemoteUserBackend']
Finally, you'll need to wire it up as WSGI middleware in wsgi.py. Your final wsgi.py
should look
something like:
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "abuse.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
import heroku_bouncer
application = heroku_bouncer.bouncer(application)
Integration with other things
I don't know how to do other things! Please send me a pull request.
Contributing
Work happens on Github. Please send me a pull request!