Adds oauth support up until authorization
This commit is contained in:
parent
be7f90b3f5
commit
d41c6a5349
@ -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.
|
||||
|
@ -130,7 +130,36 @@ 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]
|
||||
|
||||
|
||||
######################################################
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user