From: xray7224 Date: Mon, 8 Jul 2013 19:35:03 +0000 (+0100) Subject: Adds oauth support up until authorization X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=d41c6a5349db0ac573e8f0d29d239febc705f7c9;p=mediagoblin.git Adds oauth support up until authorization --- diff --git a/docs/source/api/client_register.rst b/docs/source/api/client_register.rst index 088eb51d..4ad7908e 100644 --- a/docs/source/api/client_register.rst +++ b/docs/source/api/client_register.rst @@ -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. diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index daee9295..8a71aa09 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -130,7 +130,36 @@ class Client(Base): else: return "".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] ###################################################### diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py index 6a75628e..f7e6f72c 100644 --- a/mediagoblin/federation/routing.py +++ b/mediagoblin/federation/routing.py @@ -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" + ) diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py index 743fd142..6c000855 100644 --- a/mediagoblin/federation/views.py +++ b/mediagoblin/federation/views.py @@ -14,13 +14,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py index ee342eae..ed903ce0 100644 --- a/mediagoblin/tools/request.py +++ b/mediagoblin/tools/request.py @@ -14,12 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py index 1fd242fb..db8fc388 100644 --- a/mediagoblin/tools/response.py +++ b/mediagoblin/tools/response.py @@ -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 diff --git a/setup.py b/setup.py index 6e026f30..b16f8d56 100644 --- a/setup.py +++ b/setup.py @@ -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