lemoncurry/lemonauth/views/indie.py

126 lines
4.2 KiB
Python

from annoying.decorators import render_to
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from lemoncurry import breadcrumbs, requests, utils
from urllib.parse import urlencode, urljoin, urlunparse, urlparse
from .. import tokens
from ..models import IndieAuthCode
breadcrumbs.add("lemonauth:indie", parent="home:index")
def canonical(url):
if "//" not in url:
url = "//" + url
(scheme, netloc, path, params, query, fragment) = urlparse(url)
if not scheme or scheme == "http":
scheme = "https"
if not path:
path = "/"
return urlunparse((scheme, netloc, path, params, query, fragment))
@method_decorator(csrf_exempt, name="dispatch")
class IndieView(TemplateView):
template_name = "lemonauth/indie.html"
required_params = ("client_id", "redirect_uri")
@method_decorator(login_required)
@method_decorator(render_to(template_name))
def get(self, request):
params = request.GET.dict()
params.setdefault("response_type", "id")
for param in self.required_params:
if param not in params:
return utils.bad_req("parameter {0} is required".format(param))
me = request.user.full_url
if "me" in params:
param_me = canonical(params["me"])
if me != param_me:
return utils.forbid(
"you are logged in as {}, not as {}".format(me, param_me)
)
redirect_uri = urljoin(params["client_id"], params["redirect_uri"])
type = params["response_type"]
if type not in ("id", "code"):
return utils.bad_req("unknown response_type: {0}".format(type))
scopes = ()
if type == "code":
if "scope" not in params:
return utils.bad_req("scopes required for code type")
scopes = params["scope"].split(" ")
client = requests.mf2(params["client_id"])
rels = client.to_dict()["rel-urls"].get(redirect_uri, {}).get("rels", ())
verified = "redirect_uri" in rels
try:
app = client.to_dict(filter_by_type="h-x-app")[0]["properties"]
except IndexError:
app = None
return {
"app": app,
"me": me,
"redirect_uri": redirect_uri,
"verified": verified,
"params": params,
"scopes": scopes,
"title": "indieauth from {client_id}".format(**params),
}
def post(self, request):
post = request.POST.dict()
try:
code = IndieAuthCode.objects.get(pk=post.get("code"))
except IndieAuthCode.DoesNotExist:
# if anything at all goes wrong when decoding the auth code, bail
# out immediately.
return utils.forbid("invalid auth code")
code.delete()
if code.expired:
return utils.forbid("invalid auth code")
if code.response_type != "id":
return utils.bad_req("this endpoint only supports response_type=id")
if code.client_id != post.get("client_id"):
return utils.forbid("client id did not match")
if code.redirect_uri != post.get("redirect_uri"):
return utils.forbid("redirect uri did not match")
# If we got here, it's valid! Yay!
return utils.choose_type(
request,
{"me": code.me},
{
"application/x-www-form-urlencoded": utils.form_encoded_response,
"application/json": JsonResponse,
},
)
@login_required
@require_POST
def approve(request):
params = {
"me": urljoin(utils.origin(request), request.user.url),
"code": tokens.gen_auth_code(request),
}
if "state" in request.POST:
params["state"] = request.POST["state"]
uri = request.POST["redirect_uri"]
sep = "&" if "?" in uri else "?"
return redirect(uri + sep + urlencode(params))