aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin/oauth
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin/oauth')
-rw-r--r--mediagoblin/oauth/__init__.py16
-rw-r--r--mediagoblin/oauth/exceptions.py18
-rw-r--r--mediagoblin/oauth/forms.py7
-rw-r--r--mediagoblin/oauth/oauth.py132
-rw-r--r--mediagoblin/oauth/routing.py43
-rw-r--r--mediagoblin/oauth/tools/__init__.py0
-rw-r--r--mediagoblin/oauth/tools/forms.py25
-rw-r--r--mediagoblin/oauth/tools/request.py32
-rw-r--r--mediagoblin/oauth/views.py339
9 files changed, 612 insertions, 0 deletions
diff --git a/mediagoblin/oauth/__init__.py b/mediagoblin/oauth/__init__.py
new file mode 100644
index 00000000..719b56e7
--- /dev/null
+++ b/mediagoblin/oauth/__init__.py
@@ -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/>.
+
diff --git a/mediagoblin/oauth/exceptions.py b/mediagoblin/oauth/exceptions.py
new file mode 100644
index 00000000..5eccba34
--- /dev/null
+++ b/mediagoblin/oauth/exceptions.py
@@ -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
diff --git a/mediagoblin/oauth/forms.py b/mediagoblin/oauth/forms.py
new file mode 100644
index 00000000..94c7cb52
--- /dev/null
+++ b/mediagoblin/oauth/forms.py
@@ -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")
diff --git a/mediagoblin/oauth/oauth.py b/mediagoblin/oauth/oauth.py
new file mode 100644
index 00000000..8229c47d
--- /dev/null
+++ b/mediagoblin/oauth/oauth.py
@@ -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)
diff --git a/mediagoblin/oauth/routing.py b/mediagoblin/oauth/routing.py
new file mode 100644
index 00000000..e45077bb
--- /dev/null
+++ b/mediagoblin/oauth/routing.py
@@ -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"
+ )
+
diff --git a/mediagoblin/oauth/tools/__init__.py b/mediagoblin/oauth/tools/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/mediagoblin/oauth/tools/__init__.py
diff --git a/mediagoblin/oauth/tools/forms.py b/mediagoblin/oauth/tools/forms.py
new file mode 100644
index 00000000..e3eb3298
--- /dev/null
+++ b/mediagoblin/oauth/tools/forms.py
@@ -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
diff --git a/mediagoblin/oauth/tools/request.py b/mediagoblin/oauth/tools/request.py
new file mode 100644
index 00000000..6e484bb6
--- /dev/null
+++ b/mediagoblin/oauth/tools/request.py
@@ -0,0 +1,32 @@
+# 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(","):
+ key, value = param.split("=")
+
+ key = key.lstrip(" ")
+ value = value.lstrip(" ").lstrip('"')
+ value = value.rstrip(" ").rstrip('"')
+
+ tokens[key] = value
+
+ return tokens
+
diff --git a/mediagoblin/oauth/views.py b/mediagoblin/oauth/views.py
new file mode 100644
index 00000000..116eb023
--- /dev/null
+++ b/mediagoblin/oauth/views.py
@@ -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)
+