Adds oauth support up until authorization

This commit is contained in:
xray7224 2013-07-08 20:35:03 +01:00 committed by xray7224
parent be7f90b3f5
commit d41c6a5349
7 changed files with 230 additions and 42 deletions

View File

@ -113,8 +113,8 @@ Errors
There are a number of errors you could get back, This explains what could cause some of them:
Could not decode JSON
This is caused when you have an error in your JSON, you may want to use a JSON validator to ensure that your JSON is correct.
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.

View File

@ -130,6 +130,35 @@ class Client(Base):
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=True)
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 MediaEntry(Base, MediaEntryMixin):
@ -607,10 +636,10 @@ with_polymorphic(
[ProcessingNotification, CommentNotification])
MODELS = [
User, Client, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
Notification, CommentNotification, ProcessingNotification,
CommentSubscription]
User, Client, RequestToken, AccessToken, MediaEntry, Tag, MediaTag,
MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
ProcessingNotification, CommentSubscription]
######################################################

View File

@ -16,4 +16,28 @@
from mediagoblin.tools.routing import add_route
add_route("mediagoblin.federation", "/api/client/register", "mediagoblin.federation.views:client_register")
# client registration & oauth
add_route(
"mediagoblin.federation",
"/api/client/register",
"mediagoblin.federation.views:client_register"
)
add_route(
"mediagoblin.federation",
"/oauth/request_token",
"mediagoblin.federation.views:request_token"
)
add_route(
"mediagoblin.federation",
"/oauth/authorize",
"mediagoblin.federation.views:authorize",
)
add_route(
"mediagoblin.federation",
"/oauth/access_token",
"mediagoblin.federation.views:access_token"
)

View File

@ -14,13 +14,15 @@
# 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
from oauthlib.oauth1 import RequestValidator, RequestTokenEndpoint
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.response import json_response
from mediagoblin.tools.request import decode_request
from mediagoblin.tools.response import json_response, render_400
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.db.models import Client
from mediagoblin.db.models import Client, RequestToken, AccessToken
# possible client types
client_types = ["web", "native"] # currently what pump supports
@ -28,38 +30,53 @@ client_types = ["web", "native"] # currently what pump supports
@csrf_exempt
def client_register(request):
""" Endpoint for client registration """
data = request.get_data()
if request.content_type == "application/json":
try:
data = json.loads(data)
except ValueError:
return json_response({"error":"Could not decode JSON"})
elif request.content_type == "" or request.content_type == "application/x-www-form-urlencoded":
data = request.form
else:
return json_response({"error":"Unknown Content-Type"}, status=400)
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:
return json_response({"error":"No registration type provided"}, status=400)
if "application_type" not in data or data["application_type"] not in client_types:
return json_response({"error":"Unknown application_type."}, status=400)
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:
return json_response({"error":"client_id is required to update."}, status=400)
error = "client_id is requried to update."
return json_response({"error": error}, status=400)
elif "client_secret" not in data:
return json_response({"error":"client_secret is required to update."}, status=400)
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()
client = Client.query.filter_by(
id=data["client_id"],
secret=data["client_secret"]
).first()
if client is None:
return json_response({"error":"Unauthorized."}, status=403)
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
)
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
@ -67,11 +84,14 @@ def client_register(request):
elif client_type == "client_associate":
# registering
if "client_id" in data:
return json_response({"error":"Only set client_id for update."}, status=400)
error = "Only set client_id for update."
return json_response({"error": error}, status=400)
elif "access_token" in data:
return json_response({"error":"access_token not needed for registration."}, status=400)
error = "access_token not needed for registration."
return json_response({"error": error}, status=400)
elif "client_secret" in data:
return json_response({"error":"Only set client_secret for update."}, status=400)
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
@ -85,14 +105,19 @@ def client_register(request):
secret=client_secret,
expirey=expirey_db,
application_type=data["application_type"],
)
)
else:
return json_response({"error":"Invalid registration type"}, status=400)
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):
return json_response({"error":"Logo URL {0} is not a valid URL".format(logo_url)}, status=400)
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
application_name=data.get("application_name", None)
@ -100,13 +125,15 @@ def client_register(request):
contacts = data.get("contact", None)
if contacts is not None:
if type(contacts) is not unicode:
return json_response({"error":"contacts must be a string of space-separated email addresses."}, status=400)
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
return json_response({"error":"Email {0} is not a valid email".format(contact)}, status=400)
error = "Email {0} is not a valid email.".format(contact)
return json_response({"error": error}, status=400)
client.contacts = contacts
@ -114,14 +141,16 @@ def client_register(request):
request_uri = data.get("request_uris", None)
if request_uri is not None:
if type(request_uri) is not unicode:
return json_respinse({"error":"redirect_uris must be space-separated URLs."}, status=400)
error = "redirect_uris must be space-seporated URLs."
return json_respinse({"error": error}, status=400)
request_uri = request_uri.split()
for uri in request_uri:
if not validate_url(uri):
# not a valid uri
return json_response({"error":"URI {0} is not a valid URI".format(uri)}, status=400)
error = "URI {0} is not a valid URI".format(uri)
return json_response({"error": error}, status=400)
client.request_uri = request_uri
@ -132,7 +161,85 @@ def client_register(request):
return json_response(
{
"client_id":client.id,
"client_secret":client.secret,
"expires_at":expirey,
"client_id": client.id,
"client_secret": client.secret,
"expires_at": expirey,
})
class ValidationException(Exception):
pass
class GMGRequestValidator(RequestValidator):
def __init__(self, data):
self.POST = data
def save_request_token(self, token, request):
""" Saves request token in db """
client_id = self.POST[u"Authorization"][u"oauth_consumer_key"]
request_token = RequestToken(
token=token["oauth_token"],
secret=token["oauth_token_secret"],
)
request_token.client = client_id
request_token.save()
@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 is "":
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
# Convert 'Authorization' to a dictionary
authorization = {}
for item in data["Authorization"].split(","):
key, value = item.split("=", 1)
authorization[key] = value
data[u"Authorization"] = authorization
# check the client_id
client_id = data[u"Authorization"][u"oauth_consumer_key"]
client = Client.query.filter_by(id=client_id).first()
if client is None:
# client_id is invalid
error = "Invalid client_id"
return json_response({"error": error}, status=400)
request_validator = GMGRequestValidator(data)
rv = RequestTokenEndpoint(request_validator)
tokens = rv.create_request_token(request, {})
tokenized = {}
for t in tokens.split("&"):
key, value = t.split("=")
tokenized[key] = value
# check what encoding to return them in
return json_response(tokenized)
def authorize(request):
""" Displays a page for user to authorize """
_ = 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)
# AuthorizationEndpoint
@csrf_exempt
def access_token(request):
""" Provides an access token based on a valid verifier and request token """
pass

View File

@ -14,12 +14,17 @@
# 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
from mediagoblin.db.models import User
_log = logging.getLogger(__name__)
# MIME-Types
form_encoded = "application/x-www-form-urlencoded"
json_encoded = "application/json"
def setup_user_in_request(request):
"""
Examine a request and tack on a request.user parameter if that's
@ -36,3 +41,15 @@ def setup_user_in_request(request):
# this session.
_log.warn("Killing session for user id %r", request.session['user_id'])
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:
data = request.form
else:
data = ""
return data

View File

@ -45,6 +45,15 @@ def render_error(request, status=500, title=_('Oops!'),
{'err_code': status, 'title': title, 'err_msg': err_msg}),
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):
"""Render a standard 403 page"""
_ = pass_to_ugettext

View File

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