Remove deprecated oauth 2 plugin

This commit is contained in:
Jessica Tallon 2015-01-12 16:24:36 +00:00
parent 4fd520364f
commit 247c987cf7
15 changed files with 0 additions and 1499 deletions

View File

@ -1 +0,0 @@
.. include:: ../../../mediagoblin/plugins/oauth/README.rst

View File

@ -1,148 +0,0 @@
==============
OAuth plugin
==============
.. warning::
In its current state. This plugin has received no security audit.
Development has been entirely focused on Making It Work(TM). Use this
plugin with caution.
Additionally, this and the API may break... consider it pre-alpha.
There's also a known issue that the OAuth client doesn't do
refresh tokens so this might result in issues for users.
The OAuth plugin enables third party web applications to authenticate as one or
more GNU MediaGoblin users in a safe way in order retrieve, create and update
content stored on the GNU MediaGoblin instance.
The OAuth plugin is based on the `oauth v2.25 draft`_ and is pointing by using
the ``oauthlib.oauth2.draft25.WebApplicationClient`` from oauthlib_ to a
mediagoblin instance and building the OAuth 2 provider logic around the client.
There are surely some aspects of the OAuth v2.25 draft that haven't made it
into this plugin due to the technique used to develop it.
.. _`oauth v2.25 draft`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25
.. _oauthlib: http://pypi.python.org/pypi/oauthlib
Set up the OAuth plugin
=======================
1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section::
[[mediagoblin.plugins.oauth]]
2. Run::
gmg dbupdate
in order to create and apply migrations to any database tables that the
plugin requires.
.. note::
This only enables the OAuth plugin. To be able to let clients fetch data
from the MediaGoblin instance you should also enable the API plugin or some
other plugin that supports authenticating with OAuth credentials.
Authenticate against GNU MediaGoblin
====================================
.. note::
As mentioned in `capabilities`_ GNU MediaGoblin currently only supports the
`Authorization Code Grant`_ procedure for obtaining an OAuth access token.
Authorization Code Grant
------------------------
.. note::
As mentioned in `incapabilities`_ GNU MediaGoblin currently does not
support `client registration`_
The `authorization code grant`_ works in the following way:
`Definitions`
Authorization server
The GNU MediaGoblin instance
Resource server
Also the GNU MediaGoblin instance ;)
Client
The web application intended to use the data
Redirect uri
An URI pointing to a page controlled by the *client*
Resource owner
The GNU MediaGoblin user who's resources the client requests access to
User agent
Commonly the GNU MediaGoblin user's web browser
Authorization code
An intermediate token that is exchanged for an *access token*
Access token
A secret token that the *client* uses to authenticate itself agains the
*resource server* as a specific *resource owner*.
Brief description of the procedure
++++++++++++++++++++++++++++++++++
1. The *client* requests an *authorization code* from the *authorization
server* by redirecting the *user agent* to the `Authorization Endpoint`_.
Which parameters should be included in the redirect are covered later in
this document.
2. The *authorization server* authenticates the *resource owner* and redirects
the *user agent* back to the *redirect uri* (covered later in this
document).
3. The *client* receives the request from the *user agent*, attached is the
*authorization code*.
4. The *client* requests an *access token* from the *authorization server*
5. \?\?\?\?\?
6. Profit!
Detailed description of the procedure
+++++++++++++++++++++++++++++++++++++
TBD, in the meantime here is a proof-of-concept GNU MediaGoblin client:
https://github.com/jwandborg/omgmg/
and here are some detailed descriptions from other OAuth 2
providers:
- https://developers.google.com/accounts/docs/OAuth2WebServer
- https://developers.facebook.com/docs/authentication/server-side/
and if you're unsure about anything, there's the `OAuth v2.25 draft
<http://tools.ietf.org/html/draft-ietf-oauth-v2-25>`_, the `OAuth plugin
source code
<http://gitorious.org/mediagoblin/mediagoblin/trees/master/mediagoblin/plugins/oauth>`_
and the `#mediagoblin IRC channel <http://mediagoblin.org/pages/join.html#irc>`_.
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
==============
- Only `bearer tokens`_ are issued.
- `Implicit Grant`_
- `Force TLS for token endpoint`_ - This one is up the the siteadmin
- Authorization `scope`_ and `state`
- ...
.. _`bearer tokens`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08
.. _`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
.. _`Force TLS for token endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.2

View File

@ -1,109 +0,0 @@
# 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 os
import logging
from mediagoblin.tools import pluginapi
from mediagoblin.plugins.oauth.models import OAuthToken, OAuthClient, \
OAuthUserClient
from mediagoblin.plugins.api.tools import Auth
_log = logging.getLogger(__name__)
PLUGIN_DIR = os.path.dirname(__file__)
def setup_plugin():
config = pluginapi.get_config('mediagoblin.plugins.oauth')
_log.info('Setting up OAuth...')
_log.debug('OAuth config: {0}'.format(config))
routes = [
('mediagoblin.plugins.oauth.authorize',
'/oauth-2/authorize',
'mediagoblin.plugins.oauth.views:authorize'),
('mediagoblin.plugins.oauth.authorize_client',
'/oauth-2/client/authorize',
'mediagoblin.plugins.oauth.views:authorize_client'),
('mediagoblin.plugins.oauth.access_token',
'/oauth-2/access_token',
'mediagoblin.plugins.oauth.views:access_token'),
('mediagoblin.plugins.oauth.list_connections',
'/oauth-2/client/connections',
'mediagoblin.plugins.oauth.views:list_connections'),
('mediagoblin.plugins.oauth.register_client',
'/oauth-2/client/register',
'mediagoblin.plugins.oauth.views:register_client'),
('mediagoblin.plugins.oauth.list_clients',
'/oauth-2/client/list',
'mediagoblin.plugins.oauth.views:list_clients')]
pluginapi.register_routes(routes)
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
class OAuthAuth(Auth):
def trigger(self, request):
if 'access_token' in request.GET:
return True
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 = {
'setup': setup_plugin,
'auth': OAuthAuth()
}

View File

@ -1,69 +0,0 @@
# 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 six.moves.urllib.parse import urlparse
from mediagoblin.tools.extlib.wtf_html5 import URLField
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
class AuthorizationForm(wtforms.Form):
client_id = wtforms.HiddenField(u'',
validators=[wtforms.validators.InputRequired()])
next = wtforms.HiddenField(u'', validators=[wtforms.validators.InputRequired()])
allow = wtforms.SubmitField(_(u'Allow'))
deny = wtforms.SubmitField(_(u'Deny'))
class ClientRegistrationForm(wtforms.Form):
name = wtforms.TextField(_('Name'), [wtforms.validators.InputRequired()],
description=_('The name of the OAuth client'))
description = wtforms.TextAreaField(_('Description'),
[wtforms.validators.Length(min=0, max=500)],
description=_('''This will be visible to users allowing your
application to authenticate as them.'''))
type = wtforms.SelectField(_('Type'),
[wtforms.validators.InputRequired()],
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

@ -1,158 +0,0 @@
# 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 datetime import datetime, timedelta
from sqlalchemy import (MetaData, Table, Column,
Integer, Unicode, Enum, DateTime, ForeignKey)
from sqlalchemy.ext.declarative import declarative_base
from mediagoblin.db.migration_tools import RegisterMigration
from mediagoblin.db.models import User
MIGRATIONS = {}
class OAuthClient_v0(declarative_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))
redirect_uri = Column(Unicode)
type = Column(Enum(
u'confidential',
u'public',
name=u'oauth__client_type'))
class OAuthUserClient_v0(declarative_base()):
__tablename__ = 'oauth__user_client'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey(User.id))
client_id = Column(Integer, ForeignKey(OAuthClient_v0.id))
state = Column(Enum(
u'approved',
u'rejected',
name=u'oauth__relation_state'))
class OAuthToken_v0(declarative_base()):
__tablename__ = 'oauth__tokens'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(days=30))
token = Column(Unicode, index=True)
refresh_token = Column(Unicode, index=True)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False)
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_v0(declarative_base()):
__tablename__ = 'oauth__codes'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(minutes=5))
code = Column(Unicode, index=True)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False)
class OAuthRefreshToken_v0(declarative_base()):
__tablename__ = 'oauth__refresh_tokens'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
token = Column(Unicode, index=True)
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
# XXX: Is it OK to use OAuthClient_v0.id in this way?
client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False)
@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_v0.__table__.create(db.bind)
OAuthUserClient_v0.__table__.create(db.bind)
OAuthToken_v0.__table__.create(db.bind)
OAuthCode_v0.__table__.create(db.bind)
db.commit()
@RegisterMigration(2, MIGRATIONS)
def remove_refresh_token_field(db):
metadata = MetaData(bind=db.bind)
token_table = Table('oauth__tokens', metadata, autoload=True,
autoload_with=db.bind)
refresh_token = token_table.columns['refresh_token']
refresh_token.drop()
db.commit()
@RegisterMigration(3, MIGRATIONS)
def create_refresh_token_table(db):
OAuthRefreshToken_v0.__table__.create(db.bind)
db.commit()

View File

@ -1,188 +0,0 @@
# 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 datetime import datetime, timedelta
from sqlalchemy import (
Column, Unicode, Integer, DateTime, ForeignKey, Enum)
from sqlalchemy.orm import relationship, backref
from mediagoblin.db.base import Base
from mediagoblin.db.models import User
from mediagoblin.plugins.oauth.tools import generate_identifier, \
generate_secret, generate_token, generate_code, generate_refresh_token
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,
default=generate_identifier)
secret = Column(Unicode, index=True, default=generate_secret)
owner_id = Column(Integer, ForeignKey(User.id))
owner = relationship(
User,
backref=backref('registered_clients', cascade='all, delete-orphan'))
redirect_uri = Column(Unicode)
type = Column(Enum(
u'confidential',
u'public',
name=u'oauth__client_type'))
def update_secret(self):
self.secret = generate_secret()
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=backref('oauth_client_relations',
cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id))
client = relationship(
OAuthClient,
backref=backref('oauth_user_relations', cascade='all, delete-orphan'))
state = Column(Enum(
u'approved',
u'rejected',
name=u'oauth__relation_state'))
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'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(days=30))
token = Column(Unicode, index=True, default=generate_token)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
user = relationship(
User,
backref=backref('oauth_tokens', cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
client = relationship(
OAuthClient,
backref=backref('oauth_tokens', cascade='all, delete-orphan'))
def __repr__(self):
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
self.__class__.__name__,
self.id,
self.expires.isoformat(),
self.user,
self.client)
class OAuthRefreshToken(Base):
__tablename__ = 'oauth__refresh_tokens'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
token = Column(Unicode, index=True,
default=generate_refresh_token)
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
user = relationship(User, backref=backref('oauth_refresh_tokens',
cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
client = relationship(OAuthClient,
backref=backref(
'oauth_refresh_tokens',
cascade='all, delete-orphan'))
def __repr__(self):
return '<{0} #{1} [{3}, {4}]>'.format(
self.__class__.__name__,
self.id,
self.user,
self.client)
class OAuthCode(Base):
__tablename__ = 'oauth__codes'
id = Column(Integer, primary_key=True)
created = Column(DateTime, nullable=False,
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(minutes=5))
code = Column(Unicode, index=True, default=generate_code)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
user = relationship(User, backref=backref('oauth_codes',
cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
client = relationship(OAuthClient, backref=backref(
'oauth_codes',
cascade='all, delete-orphan'))
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,
OAuthRefreshToken,
OAuthCode,
OAuthClient,
OAuthUserClient]

View File

@ -1,31 +0,0 @@
{#
# 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

@ -1,34 +0,0 @@
{#
# 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

@ -1,45 +0,0 @@
{#
# 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

@ -1,34 +0,0 @@
{#
# 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

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
# 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 uuid
from random import getrandbits
from datetime import datetime
from functools import wraps
import six
from mediagoblin.tools.response import json_response
def require_client_auth(controller):
'''
View decorator
- Requires the presence of ``?client_id``
'''
# Avoid circular import
from mediagoblin.plugins.oauth.models import OAuthClient
@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
def create_token(client, user):
'''
Create an OAuthToken and an OAuthRefreshToken entry in the database
Returns the data structure expected by the OAuth clients.
'''
from mediagoblin.plugins.oauth.models import OAuthToken, OAuthRefreshToken
token = OAuthToken()
token.user = user
token.client = client
token.save()
refresh_token = OAuthRefreshToken()
refresh_token.user = user
refresh_token.client = client
refresh_token.save()
# expire time of token in full seconds
# timedelta.total_seconds is python >= 2.7 or we would use that
td = token.expires - datetime.now()
exp_in = 86400*td.days + td.seconds # just ignore µsec
return {'access_token': token.token, 'token_type': 'bearer',
'refresh_token': refresh_token.token, 'expires_in': exp_in}
def generate_identifier():
''' Generates a ``uuid.uuid4()`` '''
return six.text_type(uuid.uuid4())
def generate_token():
''' Uses generate_identifier '''
return generate_identifier()
def generate_refresh_token():
''' Uses generate_identifier '''
return generate_identifier()
def generate_code():
''' Uses generate_identifier '''
return generate_identifier()
def generate_secret():
'''
Generate a long string of pseudo-random characters
'''
# XXX: We might not want it to use bcrypt, since bcrypt takes its time to
# generate the result.
return six.text_type(getrandbits(192))

View File

@ -1,255 +0,0 @@
# -*- coding: utf-8 -*-
# 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 logging
from six.moves.urllib.parse import urlencode
import six
from werkzeug.exceptions import BadRequest
from mediagoblin.tools.response import render_to_response, redirect, json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.plugins.oauth.models import OAuthCode, OAuthClient, \
OAuthUserClient, OAuthRefreshToken
from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm
from mediagoblin.plugins.oauth.tools import require_client_auth, \
create_token
_log = logging.getLogger(__name__)
@require_active_login
def register_client(request):
'''
Register an OAuth client
'''
form = ClientRegistrationForm(request.form)
if request.method == 'POST' and form.validate():
client = OAuthClient()
client.name = six.text_type(form.name.data)
client.description = six.text_type(form.description.data)
client.type = six.text_type(form.type.data)
client.owner_id = request.user.id
client.redirect_uri = six.text_type(form.redirect_uri.data)
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.form)
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.')
raise BadRequest()
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:
raise BadRequest()
relation.save()
return redirect(request, 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 should 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'No redirect_uri supplied!']},
_disable_cors=True)
code = OAuthCode()
code.user = request.user
code.client = client
code.save()
redirect_uri = ''.join([
redirect_uri,
'?',
urlencode({'code': code.code})])
_log.debug('Redirecting to {0}'.format(redirect_uri))
return redirect(request, location=redirect_uri)
else:
# Show prompt to allow client to access data
# - on accept: send the user agent back to the redirect_uri with the
# code parameter
# - on deny: send the user agent back to the redirect uri with error
# information
form = AuthorizationForm(request.form)
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):
'''
Access token endpoint provides access tokens to any clients that have the
right grants/credentials
'''
client = None
user = None
if request.GET.get('code'):
# Validate the code arg, then get the client object from the db.
code = OAuthCode.query.filter(OAuthCode.code ==
request.GET.get('code')).first()
if not code:
return json_response({
'error': 'invalid_request',
'error_description':
'Invalid code.'})
client = code.client
user = code.user
elif request.args.get('refresh_token'):
# Validate a refresh token, then get the client object from the db.
refresh_token = OAuthRefreshToken.query.filter(
OAuthRefreshToken.token ==
request.args.get('refresh_token')).first()
if not refresh_token:
return json_response({
'error': 'invalid_request',
'error_description':
'Invalid refresh token.'})
client = refresh_token.client
user = refresh_token.user
if client:
client_identifier = request.GET.get('client_id')
if not client_identifier:
return json_response({
'error': 'invalid_request',
'error_description':
'Missing client_id in request.'})
if not client_identifier == client.identifier:
return json_response({
'error': 'invalid_client',
'error_description':
'Mismatching client credentials.'})
if client.type == u'confidential':
client_secret = request.GET.get('client_secret')
if not client_secret:
return json_response({
'error': 'invalid_request',
'error_description':
'Missing client_secret in request.'})
if not client_secret == client.secret:
return json_response({
'error': 'invalid_client',
'error_description':
'Mismatching client credentials.'})
access_token_data = create_token(client, user)
return json_response(access_token_data, _disable_cors=True)
return json_response({
'error': 'invalid_request',
'error_description':
'Missing `code` or `refresh_token` parameter in request.'})

View File

@ -1,85 +0,0 @@
# 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 json
import pytest
import six
from six.moves.urllib.parse import parse_qs, urlparse
from mediagoblin import mg_globals
from mediagoblin.tools import processing
from mediagoblin.tests.tools import fixture_add_user
from mediagoblin.tests.test_submission import GOOD_PNG
from mediagoblin.tests import test_oauth2 as oauth
class TestHTTPCallback(object):
@pytest.fixture(autouse=True)
def setup(self, test_app):
self.test_app = test_app
self.db = mg_globals.database
self.user_password = u'secret'
self.user = fixture_add_user(u'call_back', self.user_password)
self.login()
def login(self):
self.test_app.post('/auth/login/', {
'username': self.user.username,
'password': self.user_password})
def get_access_token(self, client_id, client_secret, code):
response = self.test_app.get('/oauth-2/access_token', {
'code': code,
'client_id': client_id,
'client_secret': client_secret})
response_data = json.loads(response.body.decode())
return response_data['access_token']
def test_callback(self):
''' Test processing HTTP callback '''
self.oauth = oauth.TestOAuth()
self.oauth.setup(self.test_app)
redirect, client_id = self.oauth.test_4_authorize_confidential_client()
code = parse_qs(urlparse(redirect.location).query)['code'][0]
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == six.text_type(client_id)).first()
client_secret = client.secret
access_token = self.get_access_token(client_id, client_secret, code)
callback_url = 'https://foo.example?secrettestmediagoblinparam'
self.test_app.post('/api/submit?client_id={0}&access_token={1}\
&client_secret={2}'.format(
client_id,
access_token,
client_secret), {
'title': 'Test',
'callback_url': callback_url},
upload_files=[('file', GOOD_PNG)])
assert processing.TESTS_CALLBACKS[callback_url]['state'] == u'processed'

View File

@ -31,7 +31,6 @@ BROKER_URL = "sqlite:///%(here)s/test_user_dev/kombu.db"
[plugins]
[[mediagoblin.plugins.api]]
[[mediagoblin.plugins.oauth]]
[[mediagoblin.plugins.httpapiauth]]
[[mediagoblin.plugins.piwigo]]
[[mediagoblin.plugins.basic_auth]]

View File

@ -1,225 +0,0 @@
# 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 json
import logging
import pytest
import six
from six.moves.urllib.parse import parse_qs, urlparse
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.tests.tools import fixture_add_user
_log = logging.getLogger(__name__)
class TestOAuth(object):
@pytest.fixture(autouse=True)
def setup(self, test_app):
self.test_app = test_app
self.db = mg_globals.database
self.pman = pluginapi.PluginManager()
self.user_password = u'4cc355_70k3N'
self.user = fixture_add_user(u'joauth', self.user_password,
privileges=[u'active'])
self.login()
def login(self):
self.test_app.post(
'/auth/login/', {
'username': self.user.username,
'password': self.user_password})
def register_client(self, name, client_type, description=None,
redirect_uri=''):
return self.test_app.post(
'/oauth-2/client/register', {
'name': name,
'description': description,
'type': client_type,
'redirect_uri': redirect_uri})
def get_context(self, template_name):
return template.TEMPLATE_TEST_CONTEXT[template_name]
def test_1_public_client_registration_without_redirect_uri(self):
''' Test 'public' OAuth client registration without any redirect uri '''
response = self.register_client(
u'OMGOMGOMG', 'public', 'OMGOMG Apache License v2')
ctx = self.get_context('oauth/client/register.html')
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.name == u'OMGOMGOMG').first()
assert response.status_int == 200
# Should display an error
assert len(ctx['form'].redirect_uri.errors)
# Should not pass through
assert not client
def test_2_successful_public_client_registration(self):
''' Successfully register a public client '''
uri = 'http://foo.example'
self.register_client(
u'OMGOMG', 'public', 'OMG!', uri)
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.name == u'OMGOMG').first()
# redirect_uri should be set
assert client.redirect_uri == uri
# Client should have been registered
assert client
def test_3_successful_confidential_client_reg(self):
''' Register a confidential OAuth client '''
response = self.register_client(
u'GMOGMO', 'confidential', 'NO GMO!')
assert response.status_int == 302
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.name == u'GMOGMO').first()
# Client should have been registered
assert client
return client
def test_4_authorize_confidential_client(self):
''' Authorize a confidential client as a logged in user '''
client = self.test_3_successful_confidential_client_reg()
client_identifier = client.identifier
redirect_uri = 'https://foo.example'
response = self.test_app.get('/oauth-2/authorize', {
'client_id': client.identifier,
'scope': 'all',
'redirect_uri': redirect_uri})
# User-agent should NOT be redirected
assert response.status_int == 200
ctx = self.get_context('oauth/authorize.html')
form = ctx['form']
# Short for client authorization post reponse
capr = self.test_app.post(
'/oauth-2/client/authorize', {
'client_id': form.client_id.data,
'allow': 'Allow',
'next': form.next.data})
assert capr.status_int == 302
authorization_response = capr.follow()
assert authorization_response.location.startswith(redirect_uri)
return authorization_response, client_identifier
def get_code_from_redirect_uri(self, uri):
''' Get the value of ?code= from an URI '''
return parse_qs(urlparse(uri).query)['code'][0]
def test_token_endpoint_successful_confidential_request(self):
''' Successful request against token endpoint '''
code_redirect, client_id = self.test_4_authorize_confidential_client()
code = self.get_code_from_redirect_uri(code_redirect.location)
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == six.text_type(client_id)).first()
token_res = self.test_app.get('/oauth-2/access_token?client_id={0}&\
code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert token_res.status_int == 200
token_data = json.loads(token_res.body.decode())
assert not 'error' in token_data
assert 'access_token' in token_data
assert 'token_type' in token_data
assert 'expires_in' in token_data
assert type(token_data['expires_in']) == int
assert token_data['expires_in'] > 0
# There should be a refresh token provided in the token data
assert len(token_data['refresh_token'])
return client_id, token_data
def test_token_endpont_missing_id_confidential_request(self):
''' Unsuccessful request against token endpoint, missing client_id '''
code_redirect, client_id = self.test_4_authorize_confidential_client()
code = self.get_code_from_redirect_uri(code_redirect.location)
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == six.text_type(client_id)).first()
token_res = self.test_app.get('/oauth-2/access_token?\
code={0}&client_secret={1}'.format(code, client.secret))
assert token_res.status_int == 200
token_data = json.loads(token_res.body.decode())
assert 'error' in token_data
assert not 'access_token' in token_data
assert token_data['error'] == 'invalid_request'
assert len(token_data['error_description'])
def test_refresh_token(self):
''' Try to get a new access token using the refresh token '''
# Get an access token and a refresh token
client_id, token_data =\
self.test_token_endpoint_successful_confidential_request()
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == client_id).first()
token_res = self.test_app.get('/oauth-2/access_token',
{'refresh_token': token_data['refresh_token'],
'client_id': client_id,
'client_secret': client.secret
})
assert token_res.status_int == 200
new_token_data = json.loads(token_res.body.decode())
assert not 'error' in new_token_data
assert 'access_token' in new_token_data
assert 'token_type' in new_token_data
assert 'expires_in' in new_token_data
assert type(new_token_data['expires_in']) == int
assert new_token_data['expires_in'] > 0