PageRenderTime 37ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/appengine/auth/views.py

https://code.google.com/p/pageforest/
Python | 416 lines | 410 code | 6 blank | 0 comment | 0 complexity | 5722bbfd147876c088215c6a27651fd1 MD5 | raw file
  1. import logging
  2. import time
  3. import datetime
  4. import re
  5. from django.conf import settings
  6. from django.shortcuts import redirect
  7. from django.core.urlresolvers import reverse
  8. from django.template.loader import render_to_string
  9. from django.template import RequestContext
  10. from django.http import \
  11. HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
  12. from google.appengine.api import mail
  13. from google.appengine.ext.db import BadValueError
  14. from utils.decorators import jsonp, method_required
  15. from utils.shortcuts import render_to_response
  16. from utils.forms import ValidationError
  17. from utils import crypto
  18. from utils.json import HttpJSONResponse
  19. from utils.shortcuts import get_int, get_bool
  20. from auth import SignatureError
  21. from auth.forms import SignUpForm, SignInForm, ProfileForm
  22. from auth.models import User, CHALLENGE_EXPIRATION
  23. from auth.middleware import AccessDenied
  24. from auth.decorators import login_required
  25. from apps.models import App
  26. ENABLEJS = """
  27. <p id="enablejs" class="error">
  28. Please enable JavaScript in your browser settings and then
  29. <a href="#">reload this page</a>.<p>
  30. """
  31. HTTPONLY = """
  32. <p id="httponly" class="error" style="display:none">
  33. Security warning: Your browser does not support
  34. <a href="http://code.google.com/p/pageforest/wiki/HttpOnly">HttpOnly</a>
  35. cookies.</p>
  36. """
  37. def send_email_verification(request, user):
  38. """
  39. Send email verification to this user.
  40. """
  41. www = App.lookup('www')
  42. expires = int(time.time() + 3 * 24 * 60 * 60)
  43. verification = crypto.sign(user.get_username(),
  44. user.email,
  45. expires,
  46. www.secret)
  47. context = RequestContext(request, {'registering_user': user,
  48. 'verification': verification})
  49. message = render_to_string('auth/verify-email.txt', context)
  50. mail.send_mail(sender=settings.SITE_EMAIL_FROM,
  51. to=user.email,
  52. subject=settings.SITE_NAME + " account verification",
  53. body=message)
  54. @method_required('GET', 'POST')
  55. def email_verification(request, verification=None):
  56. """
  57. Check verification code and mark user as verified.
  58. If a verification code is not given, we show the current verified
  59. state of the signed-in user.
  60. """
  61. error = None
  62. user = None
  63. if verification:
  64. try:
  65. user = User.verify_email(verification, request.app.secret)
  66. except SignatureError, exception:
  67. error = exception.message
  68. if user and user.email_verified is None:
  69. user.email_verified = datetime.datetime.now()
  70. user.put()
  71. else:
  72. if (request.method == 'POST'
  73. and 'resend' in request.POST and request.user):
  74. send_email_verification(request, request.user)
  75. return HttpResponse('{"resent": true}',
  76. mimetype=settings.JSON_MIMETYPE_CS)
  77. # Show verification status for the currently sign-in in user.
  78. user = request.user
  79. if user is None:
  80. error = "You are not signed in."
  81. return render_to_response(request, 'auth/email-verification.html',
  82. {'verification_user': user,
  83. 'is_verified': user and user.email_verified,
  84. 'error': error})
  85. @method_required('GET', 'POST')
  86. def sign_up(request):
  87. """
  88. Create a user account on PageForest.
  89. """
  90. form = SignUpForm(request.POST or None)
  91. # Return HTML form for GET requests.
  92. if request.method == 'GET':
  93. if request.user:
  94. # The user is already signed in.
  95. return redirect(reverse(sign_in))
  96. response = render_to_response(request, 'auth/sign-up.html', {
  97. 'form': form,
  98. 'enablejs': ENABLEJS,
  99. 'httponly': HTTPONLY})
  100. response.set_cookie('httponly', 'test')
  101. return response
  102. # Return form errors as JSON.
  103. if not form.is_valid():
  104. return HttpResponse(form.errors_json(),
  105. mimetype=settings.JSON_MIMETYPE_CS)
  106. # Return empty errors object for validate.
  107. if 'validate' in request.POST:
  108. return HttpResponse('{}', mimetype=settings.JSON_MIMETYPE_CS)
  109. # Create a new user, generate a session key, return success.
  110. assert request.method == 'POST'
  111. user = form.save()
  112. send_email_verification(request, user)
  113. response = HttpJSONResponse({"statusText": "Registered"})
  114. response.set_cookie(settings.SESSION_COOKIE_NAME,
  115. user.generate_session_key(request.app),
  116. max_age=settings.SESSION_COOKIE_AGE)
  117. return response
  118. app_username_match = re.compile(settings.APP_USERNAME_REGEX).match
  119. @method_required('POST')
  120. def app_sign_up(request):
  121. """
  122. Allow 3rd party app to create private accounts directly. They must
  123. have user account names of the form: appid_username
  124. username: string
  125. secret: sha1-string
  126. email: string
  127. verifyEmail: boolean
  128. """
  129. validate_only = get_bool(request.POST, 'validate', False)
  130. username = request.POST.get('username', '')
  131. try:
  132. if not app_username_match(username):
  133. raise ValidationError().add_error('username', "Invalid username: '%s'" % username)
  134. user_app_id = username.split('_')[0]
  135. if user_app_id != request.app.get_app_id():
  136. raise ValidationError().add_error('username',
  137. "Application prefix must be '%s' (not '%s')" % (request.app.get_app_id(),
  138. user_app_id))
  139. if request.POST.get('email', '') == '':
  140. raise ValidationError().add_error('email', "Email address is required.")
  141. if User.lookup(username):
  142. raise ValidationError().add_error('username', "Username %s already exists." % username,
  143. status=409)
  144. user = User(key_name=username.lower(),
  145. username=username,
  146. email=request.POST['email'],
  147. password=request.POST.get('secret', ''))
  148. if validate_only:
  149. user.validate()
  150. else:
  151. user.put()
  152. except (ValueError, BadValueError), error:
  153. result = {'statusText': unicode(error)}
  154. status = 400
  155. if hasattr(error, 'status'):
  156. status = error.status
  157. if hasattr(error, 'errors'):
  158. result['errors'] = error.errors
  159. return HttpJSONResponse(result, status=status)
  160. return HttpJSONResponse({'statusText': "User %s created." % username},
  161. status=201)
  162. @login_required
  163. @method_required('GET', 'POST')
  164. def account(request, username):
  165. """
  166. Edit your account information.
  167. """
  168. req_username = request.user.get_username()
  169. if username is None:
  170. username = req_username
  171. user = User.lookup(username)
  172. if user is None or \
  173. not (request.user.is_admin or
  174. req_username in settings.SUPER_USERS or
  175. req_username == username):
  176. return AccessDenied(request)
  177. if request.method == 'POST':
  178. form = ProfileForm(request.POST)
  179. else:
  180. form = ProfileForm(initial=user.get_form_dict())
  181. form.enable_fields(request.user)
  182. # Return HTML form for GET requests.
  183. if request.method == 'GET':
  184. response = render_to_response(request, 'auth/account.html', {
  185. 'form': form,
  186. 'enablejs': ENABLEJS,
  187. 'httponly': HTTPONLY})
  188. response.set_cookie('httponly', 'test')
  189. return response
  190. # Return form errors as JSON.
  191. if not form.is_valid():
  192. return HttpJSONResponse(form.errors_dict())
  193. # Return empty errors object for validate.
  194. if 'validate' in request.POST:
  195. return HttpJSONResponse({})
  196. form.save()
  197. # TODO: Enable changing email address - send to old and new
  198. # and record last verified address - puts account into unverified state?
  199. # send_email_verification(request, user)
  200. return HttpJSONResponse({'statusText': "Updated."})
  201. @method_required('GET', 'POST')
  202. def sign_in(request, app_id=None):
  203. """
  204. Check credentials and generate session key(s).
  205. Sign in to:
  206. - www.pageforest.com
  207. - app_id.pageforest.com (if given)
  208. Note: return the application session key to the client via
  209. ajax, so they can request the cookie on the proper domain.
  210. This form should only ever be displayed on www.pageforest.com.
  211. TODO: Generate long-term reauthorization cookies on
  212. path=/auth/reauth so clients can upate their shorter
  213. session keys.
  214. """
  215. form = SignInForm(request.POST or None)
  216. app = None
  217. if app_id:
  218. app = App.lookup(app_id)
  219. if app is None or app.is_www():
  220. return HttpResponseRedirect(reverse(sign_in))
  221. if app is None:
  222. del form.fields['appauth']
  223. else:
  224. form.fields['appauth'].label = app_id.title()
  225. if request.method == 'GET':
  226. app_session_key = None
  227. if app and request.user:
  228. app_session_key = request.user.generate_session_key(app)
  229. response = render_to_response(request, 'auth/sign-in.html',
  230. {
  231. 'form': form,
  232. 'cross_app': app,
  233. 'session_key': app_session_key,
  234. 'enablejs': ENABLEJS
  235. })
  236. response.set_cookie('httponly', 'test')
  237. return response
  238. assert request.method == 'POST'
  239. assert request.app.is_www(), \
  240. "Sign-in is only allowed on www.%s." % settings.DEFAULT_DOMAIN
  241. # Return form errors as JSON.
  242. if not form.is_valid():
  243. if '__all__' in form.errors:
  244. form.errors['password'] = form.errors['__all__']
  245. del form.errors['__all__']
  246. return HttpResponse(form.errors_json(),
  247. mimetype=settings.JSON_MIMETYPE_CS)
  248. user = form.cleaned_data['user']
  249. json_dict = {'status': 200,
  250. 'statusText': "Authenticated",
  251. }
  252. # If we've authorized the cross-app, set the app session key cookie.
  253. if app and 'appauth' in form.cleaned_data and \
  254. form.cleaned_data['appauth']:
  255. json_dict['sessionKey'] = user.generate_session_key(app)
  256. response = HttpJSONResponse(json_dict)
  257. # Whenever we sign in - generate a fresh www session key
  258. session_key = user.generate_session_key(request.app)
  259. response.set_cookie(settings.SESSION_COOKIE_NAME, session_key,
  260. max_age=settings.SESSION_COOKIE_AGE)
  261. return response
  262. @jsonp
  263. @method_required('GET')
  264. def get_username(request):
  265. """
  266. Get the username that is currently signed in.
  267. """
  268. if request.user is None:
  269. raise Http404("The user is not signed in.")
  270. return HttpResponse(request.user.username, mimetype='text/plain')
  271. @jsonp
  272. @method_required('GET')
  273. def set_session_cookie(request, session_key):
  274. """
  275. When a valid session key for the current application is passed on
  276. the URL, set the cookie for the session key.
  277. """
  278. response = HttpResponse(session_key, content_type='text/plain')
  279. if session_key == 'expired':
  280. logging.info("Deleting session cookie")
  281. response.delete_cookie(settings.SESSION_USER_NAME,
  282. path=settings.SESSION_COOKIE_PATH)
  283. response.delete_cookie(settings.SESSION_COOKIE_NAME,
  284. path=settings.SESSION_COOKIE_PATH)
  285. else:
  286. try:
  287. user = User.verify_session_key(session_key, request.app)
  288. except SignatureError, error:
  289. return AccessDenied(request,
  290. "Invalid session key: %s" % unicode(error))
  291. response.set_cookie(settings.SESSION_USER_NAME, user.get_username(),
  292. path=settings.SESSION_COOKIE_PATH,
  293. max_age=settings.SESSION_COOKIE_AGE)
  294. response.set_cookie(settings.SESSION_COOKIE_NAME, session_key,
  295. path=settings.SESSION_COOKIE_PATH,
  296. max_age=settings.SESSION_COOKIE_AGE)
  297. return response
  298. @method_required('GET')
  299. def sign_out(request, app_id=None):
  300. """
  301. Remove the www.pageforest.com session key cookie.
  302. TODO: De-authorize the application if given.
  303. """
  304. kwargs = app_id and {'app_id': app_id} or None
  305. response = HttpResponseRedirect(reverse(sign_in, kwargs=kwargs))
  306. response.delete_cookie(settings.SESSION_COOKIE_NAME)
  307. return response
  308. @jsonp
  309. @method_required('GET')
  310. def challenge(request):
  311. """
  312. Generate a random signed challenge for login.
  313. Challenge is S(random/expires/ip, app.secret)
  314. """
  315. random_key = crypto.random64(32)
  316. expires = int(time.time() + CHALLENGE_EXPIRATION)
  317. ip = request.META.get('REMOTE_ADDR', '0.0.0.0')
  318. challenge_string = crypto.sign(random_key, expires, ip, request.app.secret)
  319. return HttpResponse(challenge_string, mimetype='text/plain')
  320. @jsonp
  321. @method_required('GET')
  322. def verify(request, signature):
  323. """
  324. Check the challenge signature with the shared user secret.
  325. If successful, return a session key and re-auth cookie.
  326. To protect against replay, we stored any SUCCESSFULLY used
  327. challenge in memcache until it's expiration time.
  328. When memcache is unavailable, replays will be allowed
  329. (but authentic challenges will still be able to succeed).
  330. Signature is:
  331. username/S(S(random/expires/ip, app.secret), S(user, pass)) =
  332. username/random/expires/ip/App-Signature/User-Signature
  333. """
  334. try:
  335. user = User.verify_signature(
  336. signature, request.app, request.META.get('REMOTE_ADDR', '0.0.0.0'))
  337. except SignatureError, error:
  338. return HttpResponseForbidden(
  339. "Invalid signature: " + unicode(error), content_type='text/plain')
  340. # Return fresh session key and reauth cookie.
  341. session_key = user.generate_session_key(
  342. request.app, subdomain=request.subdomain)
  343. reauth_cookie = user.generate_session_key(
  344. request.app, subdomain=request.subdomain,
  345. seconds=settings.REAUTH_COOKIE_AGE)
  346. response = HttpResponse(session_key, content_type='text/plain')
  347. response.set_cookie(settings.SESSION_USER_NAME, user.get_username(),
  348. path=settings.SESSION_COOKIE_PATH,
  349. max_age=settings.SESSION_COOKIE_AGE)
  350. response.set_cookie(settings.SESSION_COOKIE_NAME, session_key,
  351. max_age=settings.SESSION_COOKIE_AGE)
  352. response.set_cookie(settings.REAUTH_COOKIE_NAME, reauth_cookie,
  353. max_age=settings.REAUTH_COOKIE_AGE)
  354. return response