Added client registration caps to OAuth plugin

THE MIGRATIONS SUPPLIED WITH THIS COMMIT WILL DROP AND RE-CREATE YOUR
oauth__tokens AND oauth__codes TABLES. ALL YOUR OAUTH CODES AND TOKENS
WILL BE LOST.

- Fixed pylint issues in db/sql/migrations.
- Added __repr__ to the User model.
- Added _disable_cors option to json_response.
- Added crude error handling to the api.tools.api_auth decorator
- Updated the OAuth README.
- Added client registration, client overview, connection overview,
  client authorization views and templates.
- Added error handling to the OAuthAuth Auth object.
- Added AuthorizationForm, ClientRegistrationForm in oauth/forms.
- Added migrations for OAuth, added client registration migration.
- Added OAuthClient, OAuthUserClient models.
- Added oauth/tools with require_client_auth decorator method.
This commit is contained in:
Joar Wandborg 2012-09-21 13:02:35 +02:00
parent d4c066abf0
commit 88a9662be4
14 changed files with 602 additions and 44 deletions

View File

@ -19,12 +19,9 @@ import datetime
from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
Integer, Unicode, UnicodeText, DateTime, ForeignKey)
from mediagoblin.db.sql.util import RegisterMigration
from mediagoblin.db.sql.models import MediaEntry, Collection, User
MIGRATIONS = {}
@ -66,6 +63,7 @@ def add_transcoding_progress(db_conn):
col.create(media_entry)
db_conn.commit()
@RegisterMigration(4, MIGRATIONS)
def add_collection_tables(db_conn):
metadata = MetaData(bind=db_conn.bind)
@ -92,6 +90,7 @@ def add_collection_tables(db_conn):
db_conn.commit()
@RegisterMigration(5, MIGRATIONS)
def add_mediaentry_collected(db_conn):
metadata = MetaData(bind=db_conn.bind)
@ -102,4 +101,3 @@ def add_mediaentry_collected(db_conn):
col = Column('collected', Integer, default=0)
col.create(media_entry)
db_conn.commit()

View File

@ -85,6 +85,14 @@ class User(Base, UserMixin):
_id = SimpleFieldAlias("id")
def __repr__(self):
return '<{0} #{1} {2} {3} "{4}">'.format(
self.__class__.__name__,
self.id,
'verified' if self.email_verified else 'non-verified',
'admin' if self.is_admin else 'user',
self.username)
class MediaEntry(Base, MediaEntryMixin):
"""

View File

@ -52,7 +52,7 @@ class Auth(object):
raise NotImplemented()
def json_response(serializable, *args, **kw):
def json_response(serializable, _disable_cors=False, *args, **kw):
'''
Serializes a json objects and returns a webob.Response object with the
serialized value as the response body and Content-Type: application/json.
@ -64,11 +64,14 @@ def json_response(serializable, *args, **kw):
'''
response = Response(json.dumps(serializable), *args, **kw)
response.headers['Content-Type'] = 'application/json'
if not _disable_cors:
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
response.headers.update(cors_headers)
return response
@ -149,6 +152,11 @@ def api_auth(controller):
auth, request.url))
if not auth(request, *args, **kw):
if getattr(auth, 'errors', []):
return json_response({
'status': 403,
'errors': auth.errors})
return exc.HTTPForbidden()
return controller(request, *args, **kw)

View File

@ -122,20 +122,21 @@ Capabilities
- `Authorization endpoint`_ - Located at ``/oauth/authorize``
- `Token endpoint`_ - Located at ``/oauth/access_token``
- `Authorization Code Grant`_
- `Client Registration`_
.. _`Authorization endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.1
.. _`Token endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.2
.. _`Authorization Code Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.1
.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2
Incapabilities
==============
- `Client Registration`_ - `planned feature
<http://issues.mediagoblin.org/ticket/497>`_
- Only `bearer tokens`_ are issued.
- `Access Token Scope`_
- `Implicit Grant`_
- ...
.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2
.. _`bearer tokens`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08
.. _`Access Token Scope`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3
.. _`Implicit Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.2

View File

@ -18,11 +18,10 @@ import os
import logging
from routes.route import Route
from webob import exc
from mediagoblin.tools import pluginapi
from mediagoblin.tools.response import render_to_response
from mediagoblin.plugins.oauth.models import OAuthToken
from mediagoblin.plugins.oauth.models import OAuthToken, OAuthClient, \
OAuthUserClient
from mediagoblin.plugins.api.tools import Auth
_log = logging.getLogger(__name__)
@ -39,8 +38,19 @@ def setup_plugin():
routes = [
Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize',
controller='mediagoblin.plugins.oauth.views:authorize'),
Route('mediagoblin.plugins.oauth.authorize_client', '/oauth/client/authorize',
controller='mediagoblin.plugins.oauth.views:authorize_client'),
Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token',
controller='mediagoblin.plugins.oauth.views:access_token')]
controller='mediagoblin.plugins.oauth.views:access_token'),
Route('mediagoblin.plugins.oauth.access_token',
'/oauth/client/connections',
controller='mediagoblin.plugins.oauth.views:list_connections'),
Route('mediagoblin.plugins.oauth.register_client',
'/oauth/client/register',
controller='mediagoblin.plugins.oauth.views:register_client'),
Route('mediagoblin.plugins.oauth.list_clients',
'/oauth/client/list',
controller='mediagoblin.plugins.oauth.views:list_clients')]
pluginapi.register_routes(routes)
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
@ -54,17 +64,42 @@ class OAuthAuth(Auth):
return False
def __call__(self, request, *args, **kw):
self.errors = []
# TODO: Add suport for client credentials authorization
client_id = request.GET.get('client_id') # TODO: Not used
client_secret = request.GET.get('client_secret') # TODO: Not used
access_token = request.GET.get('access_token')
_log.debug('Authorizing request {0}'.format(request.url))
if access_token:
token = OAuthToken.query.filter(OAuthToken.token == access_token)\
.first()
if not token:
self.errors.append('Invalid access token')
return False
_log.debug('Access token: {0}'.format(token))
_log.debug('Client: {0}'.format(token.client))
relation = OAuthUserClient.query.filter(
(OAuthUserClient.user == token.user)
& (OAuthUserClient.client == token.client)
& (OAuthUserClient.state == u'approved')).first()
_log.debug('Relation: {0}'.format(relation))
if not relation:
self.errors.append(
u'Client has not been approved by the resource owner')
return False
request.user = token.user
return True
self.errors.append(u'No access_token specified')
return False
hooks = {

View File

@ -0,0 +1,70 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import wtforms
from urlparse import urlparse
from mediagoblin.tools.extlib.wtf_html5 import URLField
from mediagoblin.tools.translate import fake_ugettext_passthrough as _
class AuthorizationForm(wtforms.Form):
client_id = wtforms.HiddenField(_(u'Client ID'),
[wtforms.validators.Required()])
next = wtforms.HiddenField(_(u'Next URL'),
[wtforms.validators.Required()])
allow = wtforms.SubmitField(_(u'Allow'))
deny = wtforms.SubmitField(_(u'Deny'))
class ClientRegistrationForm(wtforms.Form):
name = wtforms.TextField(_('Name'), [wtforms.validators.Required()],
description=_('The name of the OAuth client'))
description = wtforms.TextAreaField(_('Description'),
[wtforms.validators.Length(min=0, max=500)],
description=_('''This will be visisble to users allowing your
appplication to authenticate as them.'''))
type = wtforms.SelectField(_('Type'),
[wtforms.validators.Required()],
choices=[
('confidential', 'Confidential'),
('public', 'Public')],
description=_('''<strong>Confidential</strong> - The client can
make requests to the GNU MediaGoblin instance that can not be
intercepted by the user agent (e.g. server-side client).<br />
<strong>Public</strong> - The client can't make confidential
requests to the GNU MediaGoblin instance (e.g. client-side
JavaScript client).'''))
redirect_uri = URLField(_('Redirect URI'),
[wtforms.validators.Optional(), wtforms.validators.URL()],
description=_('''The redirect URI for the applications, this field
is <strong>required</strong> for public clients.'''))
def __init__(self, *args, **kw):
wtforms.Form.__init__(self, *args, **kw)
def validate(self):
if not wtforms.Form.validate(self):
return False
if self.type.data == 'public' and not self.redirect_uri.data:
self.redirect_uri.errors.append(
_('This field is required for public clients'))
return False
return True

View File

@ -0,0 +1,46 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sqlalchemy import MetaData, Table
from mediagoblin.db.sql.util import RegisterMigration
from mediagoblin.plugins.oauth.models import OAuthClient, OAuthToken, \
OAuthUserClient, OAuthCode
MIGRATIONS = {}
@RegisterMigration(1, MIGRATIONS)
def remove_and_replace_token_and_code(db):
metadata = MetaData(bind=db.bind)
token_table = Table('oauth__tokens', metadata, autoload=True,
autoload_with=db.bind)
token_table.drop()
code_table = Table('oauth__codes', metadata, autoload=True,
autoload_with=db.bind)
code_table.drop()
OAuthClient.__table__.create(db.bind)
OAuthUserClient.__table__.create(db.bind)
OAuthToken.__table__.create(db.bind)
OAuthCode.__table__.create(db.bind)
db.commit()

View File

@ -14,15 +14,84 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import uuid
import bcrypt
from datetime import datetime, timedelta
from mediagoblin.db.sql.base import Base
from mediagoblin.db.sql.models import User
from sqlalchemy import (
Column, Unicode, Integer, DateTime, ForeignKey)
Column, Unicode, Integer, DateTime, ForeignKey, Enum)
from sqlalchemy.orm import relationship
# Don't remove this, I *think* it applies sqlalchemy-migrate functionality onto
# the models.
from migrate import changeset
class OAuthClient(Base):
__tablename__ = 'oauth__client'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
name = Column(Unicode)
description = Column(Unicode)
identifier = Column(Unicode, unique=True, index=True)
secret = Column(Unicode, index=True)
owner_id = Column(Integer, ForeignKey(User.id))
owner = relationship(User, backref='registered_clients')
redirect_uri = Column(Unicode)
type = Column(Enum(
u'confidential',
u'public'))
def generate_identifier(self):
self.identifier = unicode(uuid.uuid4())
def generate_secret(self):
self.secret = unicode(
bcrypt.hashpw(
unicode(uuid.uuid4()),
bcrypt.gensalt()))
def __repr__(self):
return '<{0} {1}:{2} ({3})>'.format(
self.__class__.__name__,
self.id,
self.name.encode('ascii', 'replace'),
self.owner.username.encode('ascii', 'replace'))
class OAuthUserClient(Base):
__tablename__ = 'oauth__user_client'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey(User.id))
user = relationship(User, backref='oauth_clients')
client_id = Column(Integer, ForeignKey(OAuthClient.id))
client = relationship(OAuthClient, backref='users')
state = Column(Enum(
u'approved',
u'rejected'))
def __repr__(self):
return '<{0} #{1} {2} [{3}, {4}]>'.format(
self.__class__.__name__,
self.id,
self.state.encode('ascii', 'replace'),
self.user,
self.client)
class OAuthToken(Base):
__tablename__ = 'oauth__tokens'
@ -39,6 +108,17 @@ class OAuthToken(Base):
index=True)
user = relationship(User)
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
client = relationship(OAuthClient)
def __repr__(self):
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
self.__class__.__name__,
self.id,
self.expires.isoformat(),
self.user,
self.client)
class OAuthCode(Base):
__tablename__ = 'oauth__codes'
@ -54,5 +134,20 @@ class OAuthCode(Base):
index=True)
user = relationship(User)
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
client = relationship(OAuthClient)
MODELS = [OAuthToken, OAuthCode]
def __repr__(self):
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
self.__class__.__name__,
self.id,
self.expires.isoformat(),
self.user,
self.client)
MODELS = [
OAuthToken,
OAuthCode,
OAuthClient,
OAuthUserClient]

View File

@ -0,0 +1,31 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#, se, seee
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.oauth.authorize_client') }}"
method="POST">
<div class="form_box_xl">
{{ csrf_token }}
<h2>Authorize {{ client.name }}?</h2>
<p class="client-description">{{ client.description }}</p>
{{ wtforms_util.render_divs(form) }}
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,34 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<h1>{% trans %}OAuth client connections{% endtrans %}</h1>
{% if connections %}
<ul>
{% for connection in connections %}
<li><span title="{{ connection.client.description }}">{{
connection.client.name }}</span> - {{ connection.state }}
</li>
{% endfor %}
</ul>
{% else %}
<p>You haven't connected using an OAuth client before.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,45 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<h1>{% trans %}Your OAuth clients{% endtrans %}</h1>
{% if clients %}
<ul>
{% for client in clients %}
<li>{{ client.name }}
<dl>
<dt>Type</dt>
<dd>{{ client.type }}</dd>
<dt>Description</dt>
<dd>{{ client.description }}</dd>
<dt>Identifier</dt>
<dd>{{ client.identifier }}</dd>
<dt>Secret</dt>
<dd>{{ client.secret }}</dd>
<dt>Redirect URI<dt>
<dd>{{ client.redirect_uri }}</dd>
</dl>
</li>
{% endfor %}
</ul>
{% else %}
<p>You don't have any clients yet. <a href="{{ request.urlgen('mediagoblin.plugins.oauth.register_client') }}">Add one</a>.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,34 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.oauth.register_client') }}"
method="POST">
<div class="form_box_xl">
<h1>Register OAuth client</h1>
{{ wtforms_util.render_divs(form) }}
<div class="form_submit_buttons">
{{ csrf_token }}
<input type="submit" value="{% trans %}Add{% endtrans %}"
class="button_form" />
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,43 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from functools import wraps
from mediagoblin.plugins.oauth.models import OAuthClient
from mediagoblin.plugins.api.tools import json_response
def require_client_auth(controller):
@wraps(controller)
def wrapper(request, *args, **kw):
if not request.GET.get('client_id'):
return json_response({
'status': 400,
'errors': [u'No client identifier in URL']},
_disable_cors=True)
client = OAuthClient.query.filter(
OAuthClient.identifier == request.GET.get('client_id')).first()
if not client:
return json_response({
'status': 400,
'errors': [u'No such client identifier']},
_disable_cors=True)
return controller(request, client)
return wrapper

View File

@ -21,38 +21,142 @@ from webob import exc, Response
from urllib import urlencode
from uuid import uuid4
from datetime import datetime
from functools import wraps
from mediagoblin.tools import pluginapi
from mediagoblin.tools.response import render_to_response
from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS, ERROR
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken
from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken, \
OAuthClient, OAuthUserClient
from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm
from mediagoblin.plugins.oauth.tools import require_client_auth
from mediagoblin.plugins.api.tools import json_response
_log = logging.getLogger(__name__)
@require_active_login
def authorize(request):
# TODO: Check if allowed
def register_client(request):
'''
Register an OAuth client
'''
form = ClientRegistrationForm(request.POST)
# Client is allowed by the user
if True or already_authorized:
# Generate a code
# Save the code, the client will later use it to obtain an access token
# Redirect the user agent to the redirect_uri with the code
if request.method == 'POST' and form.validate():
client = OAuthClient()
client.name = unicode(request.POST['name'])
client.description = unicode(request.POST['description'])
client.type = unicode(request.POST['type'])
client.owner_id = request.user.id
client.redirect_uri = unicode(request.POST['redirect_uri'])
if not 'redirect_uri' in request.GET:
add_message(request, ERROR, _('No redirect_uri found'))
client.generate_identifier()
client.generate_secret()
client.save()
add_message(request, SUCCESS, _('The client {0} has been registered!')\
.format(
client.name))
return redirect(request, 'mediagoblin.plugins.oauth.list_clients')
return render_to_response(
request,
'oauth/client/register.html',
{'form': form})
@require_active_login
def list_clients(request):
clients = request.db.OAuthClient.query.filter(
OAuthClient.owner_id == request.user.id).all()
return render_to_response(request, 'oauth/client/list.html',
{'clients': clients})
@require_active_login
def list_connections(request):
connections = OAuthUserClient.query.filter(
OAuthUserClient.user == request.user).all()
return render_to_response(request, 'oauth/client/connections.html',
{'connections': connections})
@require_active_login
def authorize_client(request):
form = AuthorizationForm(request.POST)
client = OAuthClient.query.filter(OAuthClient.id ==
form.client_id.data).first()
if not client:
_log.error('''No such client id as received from client authorization
form.''')
return exc.HTTPBadRequest()
if form.validate():
relation = OAuthUserClient()
relation.user_id = request.user.id
relation.client_id = form.client_id.data
if form.allow.data:
relation.state = u'approved'
elif form.deny.data:
relation.state = u'rejected'
else:
return exc.HTTPBadRequest
relation.save()
return exc.HTTPFound(location=form.next.data)
return render_to_response(
request,
'oauth/authorize.html',
{'form': form,
'client': client})
@require_client_auth
@require_active_login
def authorize(request, client):
# TODO: Get rid of the JSON responses in this view, it's called by the
# user-agent, not the client.
user_client_relation = OAuthUserClient.query.filter(
(OAuthUserClient.user == request.user)
& (OAuthUserClient.client == client))
if user_client_relation.filter(OAuthUserClient.state ==
u'approved').count():
redirect_uri = None
if client.type == u'public':
if not client.redirect_uri:
return json_response({
'status': 400,
'errors':
[u'Public clients MUST have a redirect_uri pre-set']},
_disable_cors=True)
redirect_uri = client.redirect_uri
if client.type == u'confidential':
redirect_uri = request.GET.get('redirect_uri', client.redirect_uri)
if not redirect_uri:
return json_response({
'status': 400,
'errors': [u'Can not find a redirect_uri for client: {0}'\
.format(client.name)]}, _disable_cors=True)
code = OAuthCode()
code.code = unicode(uuid4())
code.user = request.user
code.client = client
code.save()
redirect_uri = ''.join([
request.GET.get('redirect_uri'),
redirect_uri,
'?',
urlencode({'code': code.code})])
@ -65,28 +169,34 @@ def authorize(request):
# code parameter
# - on deny: send the user agent back to the redirect uri with error
# information
pass
return render_to_response(request, 'oauth/base.html', {})
form = AuthorizationForm(request.POST)
form.client_id.data = client.id
form.next.data = request.url
return render_to_response(
request,
'oauth/authorize.html',
{'form': form,
'client': client})
def access_token(request):
if request.GET.get('code'):
code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code'))\
.first()
code = OAuthCode.query.filter(OAuthCode.code ==
request.GET.get('code')).first()
if code:
token = OAuthToken()
token.token = unicode(uuid4())
token.user = code.user
token.client = code.client
token.save()
access_token_data = {
'access_token': token.token,
'token_type': 'what_do_i_use_this_for', # TODO
'token_type': 'bearer',
'expires_in':
(token.expires - datetime.now()).total_seconds(),
'refresh_token': 'This should probably be safe'}
return Response(json.dumps(access_token_data))
(token.expires - datetime.now()).total_seconds()}
return json_response(access_token_data, _disable_cors=True)
error_data = {
'error': 'Incorrect code'}