Remove deprecated oauth 2 plugin
This commit is contained in:
parent
4fd520364f
commit
247c987cf7
@ -1 +0,0 @@
|
||||
.. include:: ../../../mediagoblin/plugins/oauth/README.rst
|
@ -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
|
@ -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()
|
||||
}
|
@ -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
|
@ -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()
|
@ -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]
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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))
|
||||
|
@ -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.'})
|
@ -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'
|
@ -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]]
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user