Skip to content

Commit

Permalink
initial commit with mostly working implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
progrium committed Sep 30, 2010
0 parents commit 4a8f34d
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 0 deletions.
26 changes: 26 additions & 0 deletions README.md
@@ -0,0 +1,26 @@
! OAuth2 on App Engine

This is an implementation of an OAuth2 (draft 10ish) authorization server with example resource server. It was written to be as concise and readable as possible, so you can better see how the spec is implemented to either implement your own or use this as a starting point for your own.

!! Running it

You can run this server locally by downloading the App Engine SDK and running it with their desktop application or command line tool. Alternatively, you can deploy this on App Engine for free with a Google account.

!! Using it

Okay, you've got a working implementation of an OAuth2 server. Now what? Presumably, you're here because you're interested in providing OAuth2 for your web service. Here are your options:

* If your app is Python, use this code to get you started with your own implementation
* If your app is something else, use this code as a reference for your own implementation
* If you're altruistic, use this code as reference to build an OAuth2 library for your language
* Alternatively, you can tweak this code to BE your authorization server running on App Engine

!! Contributing to it

We're hoping this project not only helps you learn OAuth2 for your own implementations, but eventually we'd like to make it a library for Python. This requires a number of complications and abstractions that will make reading it for reference a bit more difficult. So the plan is to approach this slowly and intelligently with the least number of abstractions that do the job. Hopefully, this library will then also be a reference for libraries in other languages. This implementation itself is loosely based on an older Ruby OAuth2 server implementation/library (http://github.com/ThoughtWorksStudios/oauth2_provider).

HOWEVER, for the time being, the primary goal of this project is to provide a full implementation of the latest OAuth2 spec, which is currently at draft 10, with about 3 more planned before it becomes finalized in early 2011.

!! Authors

* Jeff Lindsay <progrium@gmail.com>
8 changes: 8 additions & 0 deletions app.yaml
@@ -0,0 +1,8 @@
application: gae-oauth
version: 1
runtime: python
api_version: 1

handlers:
- url: .*
script: main.py
11 changes: 11 additions & 0 deletions index.yaml
@@ -0,0 +1,11 @@
indexes:

# AUTOGENERATED

# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run. If you want to manage the
# index.yaml file manually, remove the above marker line (the line
# saying "# AUTOGENERATED"). If you want to manage some indexes
# manually, move them above the marker line. The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.
47 changes: 47 additions & 0 deletions main.py
@@ -0,0 +1,47 @@
from google.appengine.ext import webapp, db
from django.utils import simplejson

from google.appengine.ext.webapp import util, template
from google.appengine.api import users
import urllib

from oauth.handlers import AuthorizationHandler, AccessTokenHandler
from oauth.models import OAuth_Client

# Notes:
# Access tokens usually live shorter than access grant
# Refresh tokens usually live as long as access grant


class MainHandler(webapp.RequestHandler):
def get(self):
self.response.out.write('Hello world!')

class ClientsHandler(webapp.RequestHandler):
""" This is only indirectly necessary since the spec
calls for clients, but managing them is out of scope
"""

def get(self):
clients = OAuth_Client.all()
self.response.out.write(
template.render('templates/clients.html', locals()))

def post(self):
client = OAuth_Client(
name = self.request.get('name'),
redirect_uri = self.request.get('redirect_uri'), )
client.put()
self.redirect(self.request.path)


def main():
application = webapp.WSGIApplication([
('/', MainHandler),
('/oauth/authorize', AuthorizationHandler),
('/oauth/token', AccessTokenHandler),
('/app/clients', ClientsHandler), ],debug=True)
util.run_wsgi_app(application)

if __name__ == '__main__':
main()
Empty file added oauth/__init__.py
Empty file.
215 changes: 215 additions & 0 deletions oauth/handlers.py
@@ -0,0 +1,215 @@
from google.appengine.ext import webapp, db
from google.appengine.ext.webapp import template, util
from google.appengine.api import users
from django.utils import simplejson

import urllib

from oauth.models import OAuth_Authorization, OAuth_Token, OAuth_Client
from oauth.utils import extract

class AuthorizationHandler(webapp.RequestHandler):
SUPPORTED_RESPONSE_TYPES = [
'code',
'token',
'code_and_token', ] # NOTE: code_and_token may be removed in spec

def authz_redirect(self, query, fragment=None):
query_string = ('?%s' % urllib.urlencode(query)) if query else ''
fragment_string = ('#%s' % urllib.urlencode(fragment)) if fragment else ''
self.redirect(''.join([self.redirect_uri, query_string, fragment_string]))

def authz_error(self, code, description=None):
error = {'error': code, 'error_description': description}
if self.request.get('state'):
error['state'] = self.request.get('state')
self.authz_redirect(error)

def validate_params(self):
self.user = users.get_current_user()
if self.request.method == 'POST' and not self.user:
self.error(403)
self.response.out.write("Authentication required.")
return False

self.redirect_uri = self.request.get('redirect_uri')
if not self.redirect_uri:
self.error(400)
self.response.out.write("The parameter redirect_uri is required.")
return False
# TODO: validate url?

if not self.request.get('response_type') in self.SUPPORTED_RESPONSE_TYPES:
self.authz_error('unsupported_response_type', "The requested response type is not supported.")
return False

self.client = OAuth_Client.get_by_client_id(self.request.get('client_id'))
if not self.client:
self.authz_error('invalid_client', "The client identifier provided is invalid.")
return False

if self.client.redirect_uri:
if self.client.redirect_uri != self.redirect_uri:
self.authz_error('redirect_uri_mismatch',
"The redirection URI provided does not match a pre-registered value.")
return False

return True

@util.login_required
def get(self):
# TODO: put scope into ui
if not self.validate_params():
return
template_data = extract([
'response_type',
'redirect_uri',
'client_id',
'scope',
'state',], self.request.GET)
template_data['client'] = self.client
self.response.out.write(
template.render('templates/authorize.html', template_data))

def post(self):
if not self.validate_params():
return

# TODO: check for some sort of cross site request forgery? sign the request?

if self.request.get('authorize').lower() == 'no':
self.authz_error('access_denied', "The user did not allow authorization.")
return

response_type = self.request.get('response_type')

if response_type in ['code', 'code_and_token']:
code = OAuth_Authorization(
user_id = self.user.user_id(),
client_id = self.client.client_id,
redirect_uri = self.redirect_uri, )
code.put()
code = code.serialize(state=self.request.get('state'))
else:
code = None

if response_type in ['token', 'code_and_token']:
token = OAuth_Token(
user_id = self.user.user_id(),
client_id = self.client.client_id,
scope = self.request.get('scope'), )
token.put(can_refresh=False)
token = token.serialize(requested_scope=self.request.get('scope'))
else:
token = None

self.authz_redirect(code, token)



class AccessTokenHandler(webapp.RequestHandler):
SUPPORTED_GRANT_TYPES = [
'client_credentials',
'refresh_token',
'authorization_code',
'password',
#'none', (will require not giving refresh token) ... == client_credentials?
#'asssertion', (will require not giving refresh token)
]

def render_error(self, code, description):
self.error(400)
self.response.headers['content-type'] = 'application/json'
self.response.out.write(simplejson.dumps(
{'error': code, 'error_description': description,}))

def render_response(self, token):
self.response.headers['content-type'] = 'application/json'
self.response.out.write(simplejson.dumps(
token.serialize(requested_scope=self.request.get('scope'))))

def get(self):
""" This method MAY be supported according to spec """
self.handle()

def post(self):
""" This method MUST be supported according to spec """
self.handle()

def handle(self):
# TODO: MUST require transport-level security
client_id = self.request.get('client_id')
client_secret = self.request.get('client_secret')
grant_type = self.request.get('grant_type')
scope = self.request.get('scope')

if not grant_type in self.SUPPORTED_GRANT_TYPES:
self.render_error('unsupported_grant_type', "Grant type not supported.")
return

client = OAuth_Client.authenticate(client_id, client_secret)
if not client:
self.render_error('invalid_client', "Inavlid client credentials.")
return

# Dispatch to one of the grant handlers below
getattr(self, 'handle_%s' % grant_type)(client, scope)

def handle_password(self, client, scope=None):
# Since App Engine doesn't let you programmatically auth,
# and the local SDK environment doesn't need a password,
# we just always grant this w/out auth
# TODO: something better?

username = self.request.get('username')
password = self.request.get('password')

if not username or not password:
self.render_error('invalid_grant', "Invalid end-user credentials.")
return

token = OAuth_Token(
client_id = client.client_id,
user_id = username,
scope = scope, )
token.put()

self.render_response(token)

def handle_client_credentials(self, client, scope=None):
token = OAuth_Token(
client_id = client.client_id,
scope = scope, )
token.put(can_refresh=False)

self.render_response(token)

def handle_refresh_token(self, client, scope=None):
token = OAuth_Token.get_by_refresh_token(self.request.get('refresh_token'))

if not token or token.client_id != client.client_id:
self.render_error('invalid_grant', "Invalid refresh token.")
return

# TODO: refresh token should expire along with grant according to spec
token = token.refresh()

self.render_response(token)

def handle_authorization_code(self, client, scope=None):
authorization = OAuth_Authorization.get_by_code(self.request.get('code'))
redirect_uri = self.request.get('redirect_url')

if not authorization or not authorization.validate(code, redirect_uri, client.client_id):
self.render_error('invalid_grant', "Authorization code expired or invalid.")
return

token = OAuth_Token(
user_id = authorization.user_id,
client_id = authorization.client_id,
scope = scope, )
token.put()
authorization.delete()

self.render_response(token)

0 comments on commit 4a8f34d

Please sign in to comment.