Merge remote-tracking branch 'refs/remotes/tsyesika/master'

New oauth tools!  Heck yeah!
This commit is contained in:
Christopher Allan Webber 2013-08-15 10:34:20 -05:00
commit 4834ef8ec2
31 changed files with 1408 additions and 50 deletions

View File

@ -0,0 +1,158 @@
.. MediaGoblin Documentation
Written in 2011, 2012 by MediaGoblin contributors
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to
the public domain worldwide. This software is distributed without
any warranty.
You should have received a copy of the CC0 Public Domain
Dedication along with this software. If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
====================
Registering a Client
====================
To use the GNU MediaGoblin API you need to use the dynamic client registration. This has been adapted from the `OpenID specification <https://openid.net/specs/openid-connect-registration-1_0.html>`_, this is the only part of OpenID that is being used to serve the purpose to provide the client registration which is used in OAuth.
The endpoint is ``/api/client/register``
The parameters are:
type
**required** - This must be either *client_associate* (for new registration) or *client_update*
client_id
**update only** - This should only be used updating client information, this is the client_id given when you register
client_secret
**update only** - This should only be used updating client information, this is the client_secret given when you register
contacts
**optional** - This a space seporated list of email addresses to contact of people responsible for the client
application_type
**required** - This is the type of client you are making, this must be either *web* or *native*
application_name
**optional** - This is the name of your client
logo_url
**optional** - This is a URL of the logo image for your client
redirect_uri
**optional** - This is a space seporated list of pre-registered URLs for use at the Authorization Server
Response
--------
You will get back a response::
client_id
This identifies a client
client_secret
This is the secret.
expires_at
This is time that the client credentials expire. If this is 0 the client registration does not expire.
=======
Example
=======
Register Client
---------------
To register a client for the first time, this is the minimum you must supply::
{
"type": "client_associate",
"application_type": "native"
}
A Response will look like::
{
"client_secret": "hJtfhaQzgKerlLVdaeRAgmbcstSOBLRfgOinMxBCHcb",
"expires_at": 0,
"client_id": "vwljdhUMhhNbdKizpjZlxv"
}
Updating Client
---------------
Using the response we got above we can update the information and add new information we may have opted not to supply::
{
"type": "client_update",
"client_id": "vwljdhUMhhNbdKizpjZlxv",
"client_secret": "hJtfhaQzgKerlLVdaeRAgmbcstSOBLRfgOinMxBCHcb",
"application_type": "web",
"application_name": "MyClient!",
"logo_url": "https://myclient.org/images/my_logo.png",
"contacts": "myemail@someprovider.com another_developer@provider.net",
}
The response will just return back the client_id and client_secret you sent::
{
"client_id": "vwljdhUMhhNbdKizpjZlxv",
"client_secret": "hJtfhaQzgKerlLVdaeRAgmbcstSOBLRfgOinMxBCHcb",
"expires_at": 0
}
======
Errors
======
There are a number of errors you could get back, This explains what could cause some of them:
Could not decode data
This is caused when you have an error in the encoding of your data.
Unknown Content-Type
You should sent a Content-Type header with when you make a request, this should be either application/json or www-form-urlencoded. This is caused when a unknown Content-Type is used.
No registration type provided
This is when you leave out the ``type``. This should either be client_update or client_associate
Unknown application_type.
This is when you have provided a ``type`` however this isn't one of the known types.
client_id is required to update.
When you try and update you need to specify the client_id, this will be what you were given when you initially registered the client.
client_secret is required to update.
When you try to update you need to specify the client_secrer, this will be what you were given when you initially register the client.
Unauthorized.
This is when you are trying to update however the client_id and/or client_secret you have submitted are incorrect.
Only set client_id for update.
This should only be given when you update.
Only set client_secret for update.
This should only be given when you update.
Logo URL <url> is not a valid URL
This is when the URL specified did not meet the validation.
contacts must be a string of space-separated email addresses.
``contacts`` should be a string (not a list), ensure each email is seporated by a space
Email <email> is not a valid email
This is when you have submitted an invalid email address
redirect_uris must be space-separated URLs.
``redirect_uris`` should be a string (not a list), ensure each URL is seporated by a space
URI <URI> is not a valid URI
This is when your URI is invalid.

36
docs/source/api/oauth.rst Normal file
View File

@ -0,0 +1,36 @@
.. MediaGoblin Documentation
Written in 2011, 2012 by MediaGoblin contributors
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to
the public domain worldwide. This software is distributed without
any warranty.
You should have received a copy of the CC0 Public Domain
Dedication along with this software. If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
==============
Authentication
==============
GNU MediaGoblin uses OAuth1 to authenticate requests to the API. There are many
libraries out there for OAuth1, you're likely not going to have to do much. There
is a library for the GNU MediaGoblin called `PyPump <https://github.com/xray7224/PyPump>`_.
We are not using OAuth2 as we want to stay completely compatable with GNU MediaGoblin.
We use :doc:`client_register` to get the client ID and secret.
Endpoints
---------
These are the endpoints you need to use for the oauth requests:
`/oauth/request_token` is for getting the request token.
`/oauth/authorize` is to send the user to to authorize your application.
`/oauth/access_token` is for getting the access token to use in requests.

View File

@ -25,6 +25,8 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint from migrate.changeset.constraint import UniqueConstraint
from mediagoblin.db.extratypes import JSONEncoded
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
@ -379,3 +381,82 @@ def pw_hash_nullable(db):
constraint.create() constraint.create()
db.commit() db.commit()
# oauth1 migrations
class Client_v0(declarative_base()):
"""
Model representing a client - Used for API Auth
"""
__tablename__ = "core__clients"
id = Column(Unicode, nullable=True, primary_key=True)
secret = Column(Unicode, nullable=False)
expirey = Column(DateTime, nullable=True)
application_type = Column(Unicode, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
# optional stuff
redirect_uri = Column(JSONEncoded, nullable=True)
logo_url = Column(Unicode, nullable=True)
application_name = Column(Unicode, nullable=True)
contacts = Column(JSONEncoded, nullable=True)
def __repr__(self):
if self.application_name:
return "<Client {0} - {1}>".format(self.application_name, self.id)
else:
return "<Client {0}>".format(self.id)
class RequestToken_v0(declarative_base()):
"""
Model for representing the request tokens
"""
__tablename__ = "core__request_tokens"
token = Column(Unicode, primary_key=True)
secret = Column(Unicode, nullable=False)
client = Column(Unicode, ForeignKey(Client_v0.id))
user = Column(Integer, ForeignKey(User.id), nullable=True)
used = Column(Boolean, default=False)
authenticated = Column(Boolean, default=False)
verifier = Column(Unicode, nullable=True)
callback = Column(Unicode, nullable=False, default=u"oob")
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class AccessToken_v0(declarative_base()):
"""
Model for representing the access tokens
"""
__tablename__ = "core__access_tokens"
token = Column(Unicode, nullable=False, primary_key=True)
secret = Column(Unicode, nullable=False)
user = Column(Integer, ForeignKey(User.id))
request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class NonceTimestamp_v0(declarative_base()):
"""
A place the timestamp and nonce can be stored - this is for OAuth1
"""
__tablename__ = "core__nonce_timestamps"
nonce = Column(Unicode, nullable=False, primary_key=True)
timestamp = Column(DateTime, nullable=False, primary_key=True)
@RegisterMigration(14, MIGRATIONS)
def create_oauth1_tables(db):
""" Creates the OAuth1 tables """
Client_v0.__table__.create(db.bind)
RequestToken_v0.__table__.create(db.bind)
AccessToken_v0.__table__.create(db.bind)
NonceTimestamp_v0.__table__.create(db.bind)
db.commit()

View File

@ -105,6 +105,72 @@ class User(Base, UserMixin):
_log.info('Deleted user "{0}" account'.format(self.username)) _log.info('Deleted user "{0}" account'.format(self.username))
class Client(Base):
"""
Model representing a client - Used for API Auth
"""
__tablename__ = "core__clients"
id = Column(Unicode, nullable=True, primary_key=True)
secret = Column(Unicode, nullable=False)
expirey = Column(DateTime, nullable=True)
application_type = Column(Unicode, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
# optional stuff
redirect_uri = Column(JSONEncoded, nullable=True)
logo_url = Column(Unicode, nullable=True)
application_name = Column(Unicode, nullable=True)
contacts = Column(JSONEncoded, nullable=True)
def __repr__(self):
if self.application_name:
return "<Client {0} - {1}>".format(self.application_name, self.id)
else:
return "<Client {0}>".format(self.id)
class RequestToken(Base):
"""
Model for representing the request tokens
"""
__tablename__ = "core__request_tokens"
token = Column(Unicode, primary_key=True)
secret = Column(Unicode, nullable=False)
client = Column(Unicode, ForeignKey(Client.id))
user = Column(Integer, ForeignKey(User.id), nullable=True)
used = Column(Boolean, default=False)
authenticated = Column(Boolean, default=False)
verifier = Column(Unicode, nullable=True)
callback = Column(Unicode, nullable=False, default=u"oob")
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class AccessToken(Base):
"""
Model for representing the access tokens
"""
__tablename__ = "core__access_tokens"
token = Column(Unicode, nullable=False, primary_key=True)
secret = Column(Unicode, nullable=False)
user = Column(Integer, ForeignKey(User.id))
request_token = Column(Unicode, ForeignKey(RequestToken.token))
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class NonceTimestamp(Base):
"""
A place the timestamp and nonce can be stored - this is for OAuth1
"""
__tablename__ = "core__nonce_timestamps"
nonce = Column(Unicode, nullable=False, primary_key=True)
timestamp = Column(DateTime, nullable=False, primary_key=True)
class MediaEntry(Base, MediaEntryMixin): class MediaEntry(Base, MediaEntryMixin):
""" """
TODO: Consider fetching the media_files using join TODO: Consider fetching the media_files using join
@ -580,10 +646,10 @@ with_polymorphic(
[ProcessingNotification, CommentNotification]) [ProcessingNotification, CommentNotification])
MODELS = [ MODELS = [
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, User, Client, RequestToken, AccessToken, NonceTimestamp, MediaEntry, Tag,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
Notification, CommentNotification, ProcessingNotification, MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
CommentSubscription] ProcessingNotification, CommentSubscription]
""" """
Foundations are the default rows that are created immediately after the tables Foundations are the default rows that are created immediately after the tables

View File

@ -18,13 +18,16 @@ from functools import wraps
from urlparse import urljoin from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
from oauthlib.oauth1 import ResourceEndpoint
from mediagoblin import mg_globals as mgg from mediagoblin import mg_globals as mgg
from mediagoblin import messages from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User from mediagoblin.db.models import MediaEntry, User
from mediagoblin.tools.response import redirect, render_404 from mediagoblin.tools.response import json_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.oauth.tools.request import decode_authorization_header
from mediagoblin.oauth.oauth import GMGRequestValidator
def require_active_login(controller): def require_active_login(controller):
""" """
@ -268,3 +271,32 @@ def auth_enabled(controller):
return controller(request, *args, **kwargs) return controller(request, *args, **kwargs)
return wrapper return wrapper
def oauth_required(controller):
""" Used to wrap API endpoints where oauth is required """
@wraps(controller)
def wrapper(request, *args, **kwargs):
data = request.headers
authorization = decode_authorization_header(data)
if authorization == dict():
error = "Missing required parameter."
return json_response({"error": error}, status=400)
request_validator = GMGRequestValidator()
resource_endpoint = ResourceEndpoint(request_validator)
valid, request = resource_endpoint.validate_protected_resource_request(
uri=request.url,
http_method=request.method,
body=request.get_data(),
headers=dict(request.headers),
)
if not valid:
error = "Invalid oauth prarameter."
return json_response({"error": error}, status=400)
return controller(request, *args, **kwargs)
return wrapper

View File

@ -0,0 +1,16 @@
# 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/>.

View File

@ -0,0 +1,18 @@
# 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/>.
class ValidationException(Exception):
pass

View File

@ -0,0 +1,7 @@
import wtforms
class AuthorizeForm(wtforms.Form):
""" Form used to authorize the request token """
oauth_token = wtforms.HiddenField("oauth_token")
oauth_verifier = wtforms.HiddenField("oauth_verifier")

132
mediagoblin/oauth/oauth.py Normal file
View File

@ -0,0 +1,132 @@
# 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 oauthlib.common import Request
from oauthlib.oauth1 import RequestValidator
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
class GMGRequestValidator(RequestValidator):
enforce_ssl = False
def __init__(self, data=None, *args, **kwargs):
self.POST = data
super(GMGRequestValidator, self).__init__(*args, **kwargs)
def save_request_token(self, token, request):
""" Saves request token in db """
client_id = self.POST[u"oauth_consumer_key"]
request_token = RequestToken(
token=token["oauth_token"],
secret=token["oauth_token_secret"],
)
request_token.client = client_id
if u"oauth_callback" in self.POST:
request_token.callback = self.POST[u"oauth_callback"]
request_token.save()
def save_verifier(self, token, verifier, request):
""" Saves the oauth request verifier """
request_token = RequestToken.query.filter_by(token=token).first()
request_token.verifier = verifier["oauth_verifier"]
request_token.save()
def save_access_token(self, token, request):
""" Saves access token in db """
access_token = AccessToken(
token=token["oauth_token"],
secret=token["oauth_token_secret"],
)
access_token.request_token = request.oauth_token
request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
access_token.user = request_token.user
access_token.save()
def get_realms(*args, **kwargs):
""" Currently a stub - called when making AccessTokens """
return list()
def validate_timestamp_and_nonce(self, client_key, timestamp,
nonce, request, request_token=None,
access_token=None):
nc = NonceTimestamp.query.filter_by(timestamp=timestamp, nonce=nonce)
nc = nc.first()
if nc is None:
return True
return False
def validate_client_key(self, client_key, request):
""" Verifies client exists with id of client_key """
client = Client.query.filter_by(id=client_key).first()
if client is None:
return False
return True
def validate_access_token(self, client_key, token, request):
""" Verifies token exists for client with id of client_key """
client = Client.query.filter_by(id=client_key).first()
token = AccessToken.query.filter_by(token=token)
token = token.first()
if token is None:
return False
request_token = RequestToken.query.filter_by(token=token.request_token)
request_token = request_token.first()
if client.id != request_token.client:
return False
return True
def validate_realms(self, *args, **kwargs):
""" Would validate reals however not using these yet. """
return True # implement when realms are implemented
def get_client_secret(self, client_key, request):
""" Retrives a client secret with from a client with an id of client_key """
client = Client.query.filter_by(id=client_key).first()
return client.secret
def get_access_token_secret(self, client_key, token, request):
access_token = AccessToken.query.filter_by(token=token).first()
return access_token.secret
class GMGRequest(Request):
"""
Fills in data to produce a oauth.common.Request object from a
werkzeug Request object
"""
def __init__(self, request, *args, **kwargs):
"""
:param request: werkzeug request object
any extra params are passed to oauthlib.common.Request object
"""
kwargs["uri"] = kwargs.get("uri", request.url)
kwargs["http_method"] = kwargs.get("http_method", request.method)
kwargs["body"] = kwargs.get("body", request.get_data())
kwargs["headers"] = kwargs.get("headers", dict(request.headers))
super(GMGRequest, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,43 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.tools.routing import add_route
# client registration & oauth
add_route(
"mediagoblin.oauth",
"/api/client/register",
"mediagoblin.oauth.views:client_register"
)
add_route(
"mediagoblin.oauth",
"/oauth/request_token",
"mediagoblin.oauth.views:request_token"
)
add_route(
"mediagoblin.oauth",
"/oauth/authorize",
"mediagoblin.oauth.views:authorize",
)
add_route(
"mediagoblin.oauth",
"/oauth/access_token",
"mediagoblin.oauth.views:access_token"
)

View File

View File

@ -0,0 +1,25 @@
# 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/>.
class WTFormData(dict):
"""
Provides a WTForm usable dictionary
"""
def getlist(self, key):
v = self[key]
if not isinstance(v, (list, tuple)):
v = [v]
return v

View File

@ -0,0 +1,35 @@
# 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/>.
def decode_authorization_header(header):
""" Decodes a HTTP Authorization Header to python dictionary """
authorization = header.get("Authorization", "").lstrip(" ").lstrip("OAuth")
tokens = {}
for param in authorization.split(","):
try:
key, value = param.split("=")
except ValueError:
continue
key = key.lstrip(" ")
value = value.lstrip(" ").lstrip('"')
value = value.rstrip(" ").rstrip('"')
tokens[key] = value
return tokens

339
mediagoblin/oauth/views.py Normal file
View File

@ -0,0 +1,339 @@
# 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 datetime
from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
AccessTokenEndpoint)
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.request import decode_request
from mediagoblin.tools.response import (render_to_response, redirect,
json_response, render_400,
form_response)
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.oauth.forms import AuthorizeForm
from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
from mediagoblin.oauth.tools.request import decode_authorization_header
from mediagoblin.oauth.tools.forms import WTFormData
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
# possible client types
client_types = ["web", "native"] # currently what pump supports
@csrf_exempt
def client_register(request):
""" Endpoint for client registration """
try:
data = decode_request(request)
except ValueError:
error = "Could not decode data."
return json_response({"error": error}, status=400)
if data is "":
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
if "type" not in data:
error = "No registration type provided."
return json_response({"error": error}, status=400)
if data.get("application_type", None) not in client_types:
error = "Unknown application_type."
return json_response({"error": error}, status=400)
client_type = data["type"]
if client_type == "client_update":
# updating a client
if "client_id" not in data:
error = "client_id is requried to update."
return json_response({"error": error}, status=400)
elif "client_secret" not in data:
error = "client_secret is required to update."
return json_response({"error": error}, status=400)
client = Client.query.filter_by(
id=data["client_id"],
secret=data["client_secret"]
).first()
if client is None:
error = "Unauthorized."
return json_response({"error": error}, status=403)
client.application_name = data.get(
"application_name",
client.application_name
)
client.application_type = data.get(
"application_type",
client.application_type
)
app_name = ("application_type", client.application_name)
if app_name in client_types:
client.application_name = app_name
elif client_type == "client_associate":
# registering
if "client_id" in data:
error = "Only set client_id for update."
return json_response({"error": error}, status=400)
elif "access_token" in data:
error = "access_token not needed for registration."
return json_response({"error": error}, status=400)
elif "client_secret" in data:
error = "Only set client_secret for update."
return json_response({"error": error}, status=400)
# generate the client_id and client_secret
client_id = random_string(22) # seems to be what pump uses
client_secret = random_string(43) # again, seems to be what pump uses
expirey = 0 # for now, lets not have it expire
expirey_db = None if expirey == 0 else expirey
application_type = data["application_type"]
# save it
client = Client(
id=client_id,
secret=client_secret,
expirey=expirey_db,
application_type=application_type,
)
else:
error = "Invalid registration type"
return json_response({"error": error}, status=400)
logo_url = data.get("logo_url", client.logo_url)
if logo_url is not None and not validate_url(logo_url):
error = "Logo URL {0} is not a valid URL.".format(logo_url)
return json_response(
{"error": error},
status=400
)
else:
client.logo_url = logo_url
client.application_name = data.get("application_name", None)
contacts = data.get("contacts", None)
if contacts is not None:
if type(contacts) is not unicode:
error = "Contacts must be a string of space-seporated email addresses."
return json_response({"error": error}, status=400)
contacts = contacts.split()
for contact in contacts:
if not validate_email(contact):
# not a valid email
error = "Email {0} is not a valid email.".format(contact)
return json_response({"error": error}, status=400)
client.contacts = contacts
redirect_uris = data.get("redirect_uris", None)
if redirect_uris is not None:
if type(redirect_uris) is not unicode:
error = "redirect_uris must be space-seporated URLs."
return json_response({"error": error}, status=400)
redirect_uris = redirect_uris.split()
for uri in redirect_uris:
if not validate_url(uri):
# not a valid uri
error = "URI {0} is not a valid URI".format(uri)
return json_response({"error": error}, status=400)
client.redirect_uri = redirect_uris
client.save()
expirey = 0 if client.expirey is None else client.expirey
return json_response(
{
"client_id": client.id,
"client_secret": client.secret,
"expires_at": expirey,
})
@csrf_exempt
def request_token(request):
""" Returns request token """
try:
data = decode_request(request)
except ValueError:
error = "Could not decode data."
return json_response({"error": error}, status=400)
if data == "":
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
if not data and request.headers:
data = request.headers
data = dict(data) # mutableifying
authorization = decode_authorization_header(data)
if authorization == dict() or u"oauth_consumer_key" not in authorization:
error = "Missing required parameter."
return json_response({"error": error}, status=400)
# check the client_id
client_id = authorization[u"oauth_consumer_key"]
client = Client.query.filter_by(id=client_id).first()
if client == None:
# client_id is invalid
error = "Invalid client_id"
return json_response({"error": error}, status=400)
# make request token and return to client
request_validator = GMGRequestValidator(authorization)
rv = RequestTokenEndpoint(request_validator)
tokens = rv.create_request_token(request, authorization)
# store the nonce & timestamp before we return back
nonce = authorization[u"oauth_nonce"]
timestamp = authorization[u"oauth_timestamp"]
timestamp = datetime.datetime.fromtimestamp(float(timestamp))
nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
nc.save()
return form_response(tokens)
@require_active_login
def authorize(request):
""" Displays a page for user to authorize """
if request.method == "POST":
return authorize_finish(request)
_ = pass_to_ugettext
token = request.args.get("oauth_token", None)
if token is None:
# no token supplied, display a html 400 this time
err_msg = _("Must provide an oauth_token.")
return render_400(request, err_msg=err_msg)
oauth_request = RequestToken.query.filter_by(token=token).first()
if oauth_request is None:
err_msg = _("No request token found.")
return render_400(request, err_msg)
if oauth_request.used:
return authorize_finish(request)
if oauth_request.verifier is None:
orequest = GMGRequest(request)
request_validator = GMGRequestValidator()
auth_endpoint = AuthorizationEndpoint(request_validator)
verifier = auth_endpoint.create_verifier(orequest, {})
oauth_request.verifier = verifier["oauth_verifier"]
oauth_request.user = request.user.id
oauth_request.save()
# find client & build context
client = Client.query.filter_by(id=oauth_request.client).first()
authorize_form = AuthorizeForm(WTFormData({
"oauth_token": oauth_request.token,
"oauth_verifier": oauth_request.verifier
}))
context = {
"user": request.user,
"oauth_request": oauth_request,
"client": client,
"authorize_form": authorize_form,
}
# AuthorizationEndpoint
return render_to_response(
request,
"mediagoblin/api/authorize.html",
context
)
def authorize_finish(request):
""" Finishes the authorize """
_ = pass_to_ugettext
token = request.form["oauth_token"]
verifier = request.form["oauth_verifier"]
oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
oauth_request = oauth_request.first()
if oauth_request is None:
# invalid token or verifier
err_msg = _("No request token found.")
return render_400(request, err_msg)
oauth_request.used = True
oauth_request.updated = datetime.datetime.now()
oauth_request.save()
if oauth_request.callback == "oob":
# out of bounds
context = {"oauth_request": oauth_request}
return render_to_response(
request,
"mediagoblin/api/oob.html",
context
)
# okay we need to redirect them then!
querystring = "?oauth_token={0}&oauth_verifier={1}".format(
oauth_request.token,
oauth_request.verifier
)
return redirect(
request,
querystring=querystring,
location=oauth_request.callback
)
@csrf_exempt
def access_token(request):
""" Provides an access token based on a valid verifier and request token """
data = request.headers
parsed_tokens = decode_authorization_header(data)
if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
error = "Missing required parameter."
return json_response({"error": error}, status=400)
request.oauth_token = parsed_tokens["oauth_token"]
request_validator = GMGRequestValidator(data)
av = AccessTokenEndpoint(request_validator)
tokens = av.create_access_token(request, {})
return form_response(tokens)

View File

@ -51,30 +51,6 @@ class Auth(object):
def __call__(self, request, *args, **kw): def __call__(self, request, *args, **kw):
raise NotImplemented() raise NotImplemented()
def json_response(serializable, _disable_cors=False, *args, **kw):
'''
Serializes a json objects and returns a werkzeug Response object with the
serialized value as the response body and Content-Type: application/json.
:param serializable: A json-serializable object
Any extra arguments and keyword arguments are passed to the
Response.__init__ method.
'''
response = Response(json.dumps(serializable), *args, content_type='application/json', **kw)
if not _disable_cors:
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
for key, value in cors_headers.iteritems():
response.headers.set(key, value)
return response
def get_entry_serializable(entry, urlgen): def get_entry_serializable(entry, urlgen):
''' '''
Returns a serializable dict() of a MediaEntry instance. Returns a serializable dict() of a MediaEntry instance.

View File

@ -21,11 +21,11 @@ from os.path import splitext
from werkzeug.exceptions import BadRequest, Forbidden from werkzeug.exceptions import BadRequest, Forbidden
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from mediagoblin.tools.response import json_response
from mediagoblin.decorators import require_active_login from mediagoblin.decorators import require_active_login
from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.media_types import sniff_media from mediagoblin.media_types import sniff_media
from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable
json_response
from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \
run_process_media, new_upload_entry run_process_media, new_upload_entry

View File

@ -35,22 +35,22 @@ def setup_plugin():
routes = [ routes = [
('mediagoblin.plugins.oauth.authorize', ('mediagoblin.plugins.oauth.authorize',
'/oauth/authorize', '/oauth-2/authorize',
'mediagoblin.plugins.oauth.views:authorize'), 'mediagoblin.plugins.oauth.views:authorize'),
('mediagoblin.plugins.oauth.authorize_client', ('mediagoblin.plugins.oauth.authorize_client',
'/oauth/client/authorize', '/oauth-2/client/authorize',
'mediagoblin.plugins.oauth.views:authorize_client'), 'mediagoblin.plugins.oauth.views:authorize_client'),
('mediagoblin.plugins.oauth.access_token', ('mediagoblin.plugins.oauth.access_token',
'/oauth/access_token', '/oauth-2/access_token',
'mediagoblin.plugins.oauth.views:access_token'), 'mediagoblin.plugins.oauth.views:access_token'),
('mediagoblin.plugins.oauth.list_connections', ('mediagoblin.plugins.oauth.list_connections',
'/oauth/client/connections', '/oauth-2/client/connections',
'mediagoblin.plugins.oauth.views:list_connections'), 'mediagoblin.plugins.oauth.views:list_connections'),
('mediagoblin.plugins.oauth.register_client', ('mediagoblin.plugins.oauth.register_client',
'/oauth/client/register', '/oauth-2/client/register',
'mediagoblin.plugins.oauth.views:register_client'), 'mediagoblin.plugins.oauth.views:register_client'),
('mediagoblin.plugins.oauth.list_clients', ('mediagoblin.plugins.oauth.list_clients',
'/oauth/client/list', '/oauth-2/client/list',
'mediagoblin.plugins.oauth.views:list_clients')] 'mediagoblin.plugins.oauth.views:list_clients')]
pluginapi.register_routes(routes) pluginapi.register_routes(routes)

View File

@ -23,7 +23,7 @@ from datetime import datetime
from functools import wraps from functools import wraps
from mediagoblin.plugins.api.tools import json_response from mediagoblin.tools.response import json_response
def require_client_auth(controller): def require_client_auth(controller):

View File

@ -21,7 +21,7 @@ from urllib import urlencode
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.tools.response import render_to_response, redirect, json_response
from mediagoblin.decorators import require_active_login from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.translate import pass_to_ugettext as _
@ -31,7 +31,6 @@ from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm AuthorizationForm
from mediagoblin.plugins.oauth.tools import require_client_auth, \ from mediagoblin.plugins.oauth.tools import require_client_auth, \
create_token create_token
from mediagoblin.plugins.api.tools import json_response
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)

View File

@ -36,7 +36,8 @@ def get_url_map():
import mediagoblin.webfinger.routing import mediagoblin.webfinger.routing
import mediagoblin.listings.routing import mediagoblin.listings.routing
import mediagoblin.notifications.routing import mediagoblin.notifications.routing
import mediagoblin.oauth.routing
for route in PluginManager().get_routes(): for route in PluginManager().get_routes():
add_route(*route) add_route(*route)

View File

@ -757,3 +757,10 @@ pre {
#exif_additional_info table tr { #exif_additional_info table tr {
margin-bottom: 10px; margin-bottom: 10px;
} }
p.verifier {
text-align:center;
font-size:50px;
none repeat scroll 0% 0% rgb(221, 221, 221);
padding: 1em 0px;
}

View File

@ -0,0 +1,56 @@
{#
# 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" %}
{% block title -%}
{% trans %}Authorization{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<h1>{% trans %}Authorize{% endtrans %}</h1>
<p>
{% trans %}You are logged in as{% endtrans %}
<strong>{{user.username}}</strong>
<br /><br />
{% trans %}Do you want to authorize {% endtrans %}
{% if client.application_name -%}
<em>{{ client.application_name }}</em>
{%- else -%}
<em>{% trans %}an unknown application{% endtrans %}</em>
{%- endif %}
{% trans %} to access your account? {% endtrans %}
<br /><br />
{% trans %}Applications with access to your account can: {% endtrans %}
<ul>
<li>{% trans %}Post new media as you{% endtrans %}</li>
<li>{% trans %}See your information (e.g profile, meida, etc...){% endtrans %}</li>
<li>{% trans %}Change your information{% endtrans %}</li>
</ul>
<br />
<form method="POST">
{{ csrf_token }}
{{ authorize_form.oauth_token }}
{{ authorize_form.oauth_verifier }}
<input type="submit" value="{% trans %}Authorize{% endtrans %}">
</form>
</p>
{% endblock %}

View File

@ -0,0 +1,33 @@
{#
# 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" %}
{% block title -%}
{% trans %}Authorization Finished{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<h1>{% trans %}Authorization Complete{% endtrans %}</h1>
<h4>{% trans %}Copy and paste this into your client:{% endtrans %}</h4>
<p class="verifier">
{{ oauth_request.verifier }}
</p>
{% endblock %}

View File

@ -23,7 +23,7 @@ from mediagoblin import mg_globals
from mediagoblin.tools import processing from mediagoblin.tools import processing
from mediagoblin.tests.tools import fixture_add_user from mediagoblin.tests.tools import fixture_add_user
from mediagoblin.tests.test_submission import GOOD_PNG from mediagoblin.tests.test_submission import GOOD_PNG
from mediagoblin.tests import test_oauth as oauth from mediagoblin.tests import test_oauth2 as oauth
class TestHTTPCallback(object): class TestHTTPCallback(object):
@ -44,7 +44,7 @@ class TestHTTPCallback(object):
'password': self.user_password}) 'password': self.user_password})
def get_access_token(self, client_id, client_secret, code): def get_access_token(self, client_id, client_secret, code):
response = self.test_app.get('/oauth/access_token', { response = self.test_app.get('/oauth-2/access_token', {
'code': code, 'code': code,
'client_id': client_id, 'client_id': client_id,
'client_secret': client_secret}) 'client_secret': client_secret})

View File

@ -0,0 +1,166 @@
# 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 cgi
import pytest
from urlparse import parse_qs, urlparse
from oauthlib.oauth1 import Client
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.tests.tools import fixture_add_user
class TestOAuth(object):
MIME_FORM = "application/x-www-form-urlencoded"
MIME_JSON = "application/json"
@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 = "AUserPassword123"
self.user = fixture_add_user("OAuthy", self.user_password)
self.login()
def login(self):
self.test_app.post(
"/auth/login/", {
"username": self.user.username,
"password": self.user_password})
def register_client(self, **kwargs):
""" Regiters a client with the API """
kwargs["type"] = "client_associate"
kwargs["application_type"] = kwargs.get("application_type", "native")
return self.test_app.post("/api/client/register", kwargs)
def test_client_client_register_limited_info(self):
""" Tests that a client can be registered with limited information """
response = self.register_client()
client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert response.status_int == 200
assert client is not None
def test_client_register_full_info(self):
""" Provides every piece of information possible to register client """
query = {
"application_name": "Testificate MD",
"application_type": "web",
"contacts": "someone@someplace.com tuteo@tsengeo.lu",
"logo_url": "http://ayrel.com/utral.png",
"redirect_uris": "http://navi-kosman.lu http://gmg-yawne-oeru.lu",
}
response = self.register_client(**query)
client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert client is not None
assert client.secret == client_info["client_secret"]
assert client.application_type == query["application_type"]
assert client.redirect_uri == query["redirect_uris"].split()
assert client.logo_url == query["logo_url"]
assert client.contacts == query["contacts"].split()
def test_client_update(self):
""" Tests that you can update a client """
# first we need to register a client
response = self.register_client()
client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
# Now update
update_query = {
"type": "client_update",
"application_name": "neytiri",
"contacts": "someone@someplace.com abc@cba.com",
"logo_url": "http://place.com/picture.png",
"application_type": "web",
"redirect_uris": "http://blah.gmg/whatever https://inboxen.org/",
}
update_response = self.register_client(**update_query)
assert update_response.status_int == 200
client_info = update_response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert client.secret == client_info["client_secret"]
assert client.application_type == update_query["application_type"]
assert client.application_name == update_query["application_name"]
assert client.contacts == update_query["contacts"].split()
assert client.logo_url == update_query["logo_url"]
assert client.redirect_uri == update_query["redirect_uris"].split()
def to_authorize_headers(self, data):
headers = ""
for key, value in data.items():
headers += '{0}="{1}",'.format(key, value)
return {"Authorization": "OAuth " + headers[:-1]}
def test_request_token(self):
""" Test a request for a request token """
response = self.register_client()
client_id = response.json["client_id"]
endpoint = "/oauth/request_token"
request_query = {
"oauth_consumer_key": client_id,
"oauth_nonce": "abcdefghij",
"oauth_timestamp": 123456789.0,
"oauth_callback": "https://some.url/callback",
}
headers = self.to_authorize_headers(request_query)
headers["Content-Type"] = self.MIME_FORM
response = self.test_app.post(endpoint, headers=headers)
response = cgi.parse_qs(response.body)
# each element is a list, reduce it to a string
for key, value in response.items():
response[key] = value[0]
request_token = self.db.RequestToken.query.filter_by(
token=response["oauth_token"]
).first()
client = self.db.Client.query.filter_by(id=client_id).first()
assert request_token is not None
assert request_token.secret == response["oauth_token_secret"]
assert request_token.client == client.id
assert request_token.used == False
assert request_token.callback == request_query["oauth_callback"]

View File

@ -51,7 +51,7 @@ class TestOAuth(object):
def register_client(self, name, client_type, description=None, def register_client(self, name, client_type, description=None,
redirect_uri=''): redirect_uri=''):
return self.test_app.post( return self.test_app.post(
'/oauth/client/register', { '/oauth-2/client/register', {
'name': name, 'name': name,
'description': description, 'description': description,
'type': client_type, 'type': client_type,
@ -115,7 +115,7 @@ class TestOAuth(object):
client_identifier = client.identifier client_identifier = client.identifier
redirect_uri = 'https://foo.example' redirect_uri = 'https://foo.example'
response = self.test_app.get('/oauth/authorize', { response = self.test_app.get('/oauth-2/authorize', {
'client_id': client.identifier, 'client_id': client.identifier,
'scope': 'all', 'scope': 'all',
'redirect_uri': redirect_uri}) 'redirect_uri': redirect_uri})
@ -129,7 +129,7 @@ class TestOAuth(object):
# Short for client authorization post reponse # Short for client authorization post reponse
capr = self.test_app.post( capr = self.test_app.post(
'/oauth/client/authorize', { '/oauth-2/client/authorize', {
'client_id': form.client_id.data, 'client_id': form.client_id.data,
'allow': 'Allow', 'allow': 'Allow',
'next': form.next.data}) 'next': form.next.data})
@ -155,7 +155,7 @@ class TestOAuth(object):
client = self.db.OAuthClient.query.filter( client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first() self.db.OAuthClient.identifier == unicode(client_id)).first()
token_res = self.test_app.get('/oauth/access_token?client_id={0}&\ token_res = self.test_app.get('/oauth-2/access_token?client_id={0}&\
code={1}&client_secret={2}'.format(client_id, code, client.secret)) code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert token_res.status_int == 200 assert token_res.status_int == 200
@ -183,7 +183,7 @@ code={1}&client_secret={2}'.format(client_id, code, client.secret))
client = self.db.OAuthClient.query.filter( client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first() self.db.OAuthClient.identifier == unicode(client_id)).first()
token_res = self.test_app.get('/oauth/access_token?\ token_res = self.test_app.get('/oauth-2/access_token?\
code={0}&client_secret={1}'.format(code, client.secret)) code={0}&client_secret={1}'.format(code, client.secret))
assert token_res.status_int == 200 assert token_res.status_int == 200
@ -204,7 +204,7 @@ code={0}&client_secret={1}'.format(code, client.secret))
client = self.db.OAuthClient.query.filter( client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == client_id).first() self.db.OAuthClient.identifier == client_id).first()
token_res = self.test_app.get('/oauth/access_token', token_res = self.test_app.get('/oauth-2/access_token',
{'refresh_token': token_data['refresh_token'], {'refresh_token': token_data['refresh_token'],
'client_id': client_id, 'client_id': client_id,
'client_secret': client.secret 'client_secret': client.secret

View File

@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import string
import errno import errno
import itsdangerous import itsdangerous
import logging import logging
@ -24,6 +26,9 @@ from mediagoblin import mg_globals
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
# produces base64 alphabet
alphabet = string.ascii_letters + "-_"
base = len(alphabet)
# Use the system (hardware-based) random number generator if it exists. # Use the system (hardware-based) random number generator if it exists.
# -- this optimization is lifted from Django # -- this optimization is lifted from Django
@ -111,3 +116,13 @@ def get_timed_signer_url(namespace):
assert __itsda_secret is not None assert __itsda_secret is not None
return itsdangerous.URLSafeTimedSerializer(__itsda_secret, return itsdangerous.URLSafeTimedSerializer(__itsda_secret,
salt=namespace) salt=namespace)
def random_string(length):
""" Returns a URL safe base64 encoded crypographically strong string """
rstring = ""
for i in range(length):
n = getrandbits(6) # 6 bytes = 2^6 = 64
n = divmod(n, base)[1]
rstring += alphabet[n]
return rstring

View File

@ -14,12 +14,18 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import logging import logging
from mediagoblin.db.models import User from mediagoblin.db.models import User
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
# MIME-Types
form_encoded = "application/x-www-form-urlencoded"
json_encoded = "application/json"
def setup_user_in_request(request): def setup_user_in_request(request):
""" """
Examine a request and tack on a request.user parameter if that's Examine a request and tack on a request.user parameter if that's
@ -36,3 +42,15 @@ def setup_user_in_request(request):
# this session. # this session.
_log.warn("Killing session for user id %r", request.session['user_id']) _log.warn("Killing session for user id %r", request.session['user_id'])
request.session.delete() request.session.delete()
def decode_request(request):
""" Decodes a request based on MIME-Type """
data = request.get_data()
if request.content_type == json_encoded:
data = json.loads(data)
elif request.content_type == form_encoded or request.content_type == "":
data = request.form
else:
data = ""
return data

View File

@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import werkzeug.utils import werkzeug.utils
from werkzeug.wrappers import Response as wz_Response from werkzeug.wrappers import Response as wz_Response
from mediagoblin.tools.template import render_template from mediagoblin.tools.template import render_template
@ -31,7 +33,6 @@ def render_to_response(request, template, context, status=200):
render_template(request, template, context), render_template(request, template, context),
status=status) status=status)
def render_error(request, status=500, title=_('Oops!'), def render_error(request, status=500, title=_('Oops!'),
err_msg=_('An error occured')): err_msg=_('An error occured')):
"""Render any error page with a given error code, title and text body """Render any error page with a given error code, title and text body
@ -44,6 +45,14 @@ def render_error(request, status=500, title=_('Oops!'),
{'err_code': status, 'title': title, 'err_msg': err_msg}), {'err_code': status, 'title': title, 'err_msg': err_msg}),
status=status) status=status)
def render_400(request, err_msg=None):
""" Render a standard 400 page"""
_ = pass_to_ugettext
title = _("Bad Request")
if err_msg is None:
err_msg = _("The request sent to the server is invalid, please double check it")
return render_error(request, 400, title, err_msg)
def render_403(request): def render_403(request):
"""Render a standard 403 page""" """Render a standard 403 page"""
@ -106,3 +115,45 @@ def redirect_obj(request, obj):
Requires obj to have a .url_for_self method.""" Requires obj to have a .url_for_self method."""
return redirect(request, location=obj.url_for_self(request.urlgen)) return redirect(request, location=obj.url_for_self(request.urlgen))
def json_response(serializable, _disable_cors=False, *args, **kw):
'''
Serializes a json objects and returns a werkzeug Response object with the
serialized value as the response body and Content-Type: application/json.
:param serializable: A json-serializable object
Any extra arguments and keyword arguments are passed to the
Response.__init__ method.
'''
response = wz_Response(json.dumps(serializable), *args, content_type='application/json', **kw)
if not _disable_cors:
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
for key, value in cors_headers.iteritems():
response.headers.set(key, value)
return response
def form_response(data, *args, **kwargs):
"""
Responds using application/x-www-form-urlencoded and returns a werkzeug
Response object with the data argument as the body
and 'application/x-www-form-urlencoded' as the Content-Type.
Any extra arguments and keyword arguments are passed to the
Response.__init__ method.
"""
response = wz_Response(
data,
content_type="application/x-www-form-urlencoded",
*args,
**kwargs
)
return response

View File

@ -0,0 +1,46 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from wtforms.validators import Email, URL
def validate_email(email):
"""
Validates an email
Returns True if valid and False if invalid
"""
email_re = Email().regex
result = email_re.match(email)
if result is None:
return False
else:
return result.string
def validate_url(url):
"""
Validates a url
Returns True if valid and False if invalid
"""
url_re = URL().regex
result = url_re.match(url)
if result is None:
return False
else:
return result.string

View File

@ -63,6 +63,8 @@ setup(
'itsdangerous', 'itsdangerous',
'pytz', 'pytz',
'six', 'six',
'oauthlib',
'pypump',
## This is optional! ## This is optional!
# 'translitcodec', # 'translitcodec',
## For now we're expecting that users will install this from ## For now we're expecting that users will install this from