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))