a Django-based indieweb.org site https://00dani.me/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

indie.py 4.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. from annoying.decorators import render_to
  2. from django.contrib.auth.decorators import login_required
  3. from django.http import JsonResponse
  4. from django.shortcuts import redirect
  5. from django.utils.decorators import method_decorator
  6. from django.views.generic import TemplateView
  7. from django.views.decorators.csrf import csrf_exempt
  8. from django.views.decorators.http import require_POST
  9. from lemoncurry import breadcrumbs, requests, utils
  10. from urllib.parse import urlencode, urljoin, urlunparse, urlparse
  11. from .. import tokens
  12. from ..models import IndieAuthCode
  13. breadcrumbs.add('lemonauth:indie', parent='home:index')
  14. def canonical(url):
  15. (scheme, loc, path, params, q, fragment) = urlparse(url)
  16. if not path:
  17. path = '/'
  18. if not loc:
  19. loc, path = path, ''
  20. if not scheme:
  21. scheme = 'https'
  22. return urlunparse((scheme, loc, path, params, q, fragment))
  23. @method_decorator(csrf_exempt, name='dispatch')
  24. class IndieView(TemplateView):
  25. template_name = 'lemonauth/indie.html'
  26. required_params = ('client_id', 'redirect_uri')
  27. @method_decorator(login_required)
  28. @method_decorator(render_to(template_name))
  29. def get(self, request):
  30. params = request.GET.dict()
  31. params.setdefault('response_type', 'id')
  32. for param in self.required_params:
  33. if param not in params:
  34. return utils.bad_req(
  35. 'parameter {0} is required'.format(param)
  36. )
  37. me = request.user.full_url
  38. if 'me' in params and me != canonical(params['me']):
  39. return utils.forbid(
  40. 'you are logged in but not as {0}'.format(me)
  41. )
  42. redirect_uri = urljoin(params['client_id'], params['redirect_uri'])
  43. type = params['response_type']
  44. if type not in ('id', 'code'):
  45. return utils.bad_req(
  46. 'unknown response_type: {0}'.format(type)
  47. )
  48. scopes = ()
  49. if type == 'code':
  50. if 'scope' not in params:
  51. return utils.bad_req(
  52. 'scopes required for code type'
  53. )
  54. scopes = params['scope'].split(' ')
  55. client = requests.mf2(params['client_id'])
  56. rels = (client.to_dict()['rel-urls']
  57. .get(redirect_uri, {})
  58. .get('rels', ()))
  59. verified = 'redirect_uri' in rels
  60. try:
  61. app = client.to_dict(filter_by_type='h-x-app')[0]['properties']
  62. except IndexError:
  63. app = None
  64. return {
  65. 'app': app,
  66. 'me': me,
  67. 'redirect_uri': redirect_uri,
  68. 'verified': verified,
  69. 'params': params,
  70. 'scopes': scopes,
  71. 'title': 'indieauth from {client_id}'.format(**params),
  72. }
  73. def post(self, request):
  74. post = request.POST.dict()
  75. try:
  76. code = IndieAuthCode.objects.get(pk=post.get('code'))
  77. except IndieAuthCode.DoesNotExist:
  78. # if anything at all goes wrong when decoding the auth code, bail
  79. # out immediately.
  80. return utils.forbid('invalid auth code')
  81. code.delete()
  82. if code.expired:
  83. return utils.forbid('invalid auth code')
  84. if code.response_type != 'id':
  85. return utils.bad_req(
  86. 'this endpoint only supports response_type=id'
  87. )
  88. if code.client_id != post.get('client_id'):
  89. return utils.forbid('client id did not match')
  90. if code.redirect_uri != post.get('redirect_uri'):
  91. return utils.forbid('redirect uri did not match')
  92. # If we got here, it's valid! Yay!
  93. return utils.choose_type(request, {'me': code.me}, {
  94. 'application/x-www-form-urlencoded': utils.form_encoded_response,
  95. 'application/json': JsonResponse,
  96. })
  97. @login_required
  98. @require_POST
  99. def approve(request):
  100. params = {
  101. 'me': urljoin(utils.origin(request), request.user.url),
  102. 'code': tokens.gen_auth_code(request),
  103. }
  104. if 'state' in request.POST:
  105. params['state'] = request.POST['state']
  106. uri = request.POST['redirect_uri']
  107. sep = '&' if '?' in uri else '?'
  108. return redirect(uri + sep + urlencode(params))