import datetime
-from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
+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 = {}
col.create(media_entry)
db_conn.commit()
+
@RegisterMigration(4, MIGRATIONS)
def add_collection_tables(db_conn):
metadata = MetaData(bind=db_conn.bind)
db_conn.commit()
+
@RegisterMigration(5, MIGRATIONS)
def add_mediaentry_collected(db_conn):
metadata = MetaData(bind=db_conn.bind)
col = Column('collected', Integer, default=0)
col.create(media_entry)
db_conn.commit()
-
_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):
"""
slug = Column(Unicode)
created = Column(DateTime, nullable=False, default=datetime.datetime.now,
index=True)
- description = Column(UnicodeText)
+ description = Column(UnicodeText)
creator = Column(Integer, ForeignKey(User.id), nullable=False)
items = Column(Integer, default=0)
get_creator = relationship(User)
-
+
def get_collection_items(self, ascending=False):
order_col = CollectionItem.position
if not ascending:
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.
'''
response = Response(json.dumps(serializable), *args, **kw)
response.headers['Content-Type'] = 'application/json'
- 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)
+
+ 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
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)
- `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
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__)
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'))
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 = {
--- /dev/null
+# 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
--- /dev/null
+# 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()
# 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'
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'
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)
+
-MODELS = [OAuthToken, OAuthCode]
+MODELS = [
+ OAuthToken,
+ OAuthCode,
+ OAuthClient,
+ OAuthUserClient]
--- /dev/null
+{#
+# 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 %}
--- /dev/null
+{#
+# 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 %}
--- /dev/null
+{#
+# 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 %}
--- /dev/null
+{#
+# 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 %}
--- /dev/null
+# 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
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})])
# 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'}