Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Utilities for talking to another appengine app with authentication.""" | |
| 6 | |
| 7 __author__ = 'agable@google.com (Aaron Gable)' | |
| 8 | |
| 9 | |
| 10 import datetime | |
| 11 import hashlib | |
| 12 import functools | |
| 13 import hmac | |
| 14 import json | |
| 15 import logging | |
| 16 import time | |
| 17 | |
| 18 from google.appengine.ext import ndb | |
| 19 | |
| 20 | |
| 21 class AuthError(Exception): | |
| 22 pass | |
| 23 | |
| 24 | |
| 25 class AuthToken(ndb.Model): | |
| 26 """Represents an id/key pair for authentication. | |
| 27 | |
| 28 This app also uses the id field as the id/name (unique datastore key) of the | |
| 29 object for easy lookup. The app's own AuthToken is stored with name 'self'. | |
| 30 | |
| 31 Attributes: | |
| 32 id: The unique identity for this token. | |
| 33 token: The corresponding authentication key. | |
| 34 """ | |
| 35 id = ndb.StringProperty() | |
| 36 token = ndb.StringProperty() | |
| 37 | |
| 38 | |
| 39 def CheckHmacAuth(should_403=True): | |
|
Vadim Sh.
2013/10/04 04:38:19
single default arg in such pseudo decorators (deco
agable
2013/10/04 21:10:15
I absolutely agree. I thought about this for a whi
| |
| 40 """Decorator for webapp2 request handler methods. | |
| 41 | |
| 42 Only use on webapp2.RequestHandler methods (e.g. get, post, put). | |
| 43 | |
| 44 Expects the request to contain: | |
| 45 id: Unique ID of requester, used to get an AuthToken from ndb | |
| 46 time: Unix epoch time the request was made (to prevent replay attacks) | |
| 47 auth: The hmac(key, id+time+params, sha256).hexdigest, to authenticate | |
| 48 the request, where the key is the matching token in the ID's AuthToken | |
| 49 **params: All of the request GET/POST parameters | |
| 50 | |
| 51 If should_403 is True, simply 403s if things don't check out right. | |
| 52 Otherwise, sets request.hmac_authenticated to True or False, as appropriate. | |
| 53 """ | |
| 54 def decorator(handler): | |
| 55 """The real decorator, conditioned on should_throw.""" | |
|
Vadim Sh.
2013/10/04 04:38:19
should_403?
agable
2013/10/04 21:10:15
Done.
| |
| 56 @functools.wrap(handler) | |
|
Vadim Sh.
2013/10/04 04:38:19
wraps
agable
2013/10/04 21:10:15
Already done (I may have uploaded this patchset be
| |
| 57 def wrapper(self, *args, **kwargs): | |
| 58 """Does the real legwork and calls the wrapped handler or 403s.""" | |
| 59 def abort_auth(log_msg): | |
| 60 """Helper method to be an exit hatch when authentication fails. | |
| 61 | |
| 62 If should_403 is True, writes a 403 to the response. | |
| 63 If should_403 is False, sets hmac_authenticated to False and falls | |
| 64 through to the wrapped handler. | |
| 65 """ | |
| 66 logging.info(log_msg) | |
| 67 if should_403: | |
| 68 self.response.headers['Content-Type'] = 'text/plain' | |
| 69 self.reponse.status = 403 | |
| 70 self.response.write('403: Forbidden') | |
|
Vadim Sh.
2013/10/04 04:38:19
Same code shorter and ensuring request processing
agable
2013/10/04 21:10:15
Thanks, didn't know about that! Done.
| |
| 71 else: | |
| 72 setattr(self.request, 'hmac_authenticated', False) | |
|
iannucci
2013/10/03 22:20:51
self.request.hmac_authenticated = False
doesn't w
agable
2013/10/04 21:10:15
Done.
| |
| 73 handler(self, *args, **kwargs) | |
| 74 | |
| 75 # Get the id, time, and auth fields from the request. | |
| 76 id = self.request.get('id') | |
| 77 if not id: | |
| 78 abort_auth('No id in request.') | |
|
Vadim Sh.
2013/10/04 04:38:19
I'd rename it into 'finish_auth' or something with
agable
2013/10/04 21:10:15
I'd say that it still clearly is an abort -- of th
| |
| 79 return | |
| 80 authtoken = ndb.Key(AuthToken, id).get() | |
| 81 if not authtoken: | |
| 82 abort_auth('No auth token found for id %s.' % id) | |
| 83 return | |
| 84 key = authtoken.token | |
| 85 | |
| 86 time = self.request.get('time') | |
| 87 if not time: | |
| 88 abort_auth('No time in request.') | |
| 89 return | |
| 90 then = datetime.datetime.fromtimestamp(time) | |
| 91 now = datetime.datetime.now() | |
| 92 if abs(now - then) > datetime.timedelta(minutes=10): | |
|
Vadim Sh.
2013/10/04 04:38:19
for inter app engine communication 10 min is forev
agable
2013/10/04 21:10:15
Agreed. This isn't solely for inter-appengine comm
| |
| 93 abort_auth('Time more than 10 minutes out of sync.') | |
| 94 return | |
| 95 | |
| 96 auth = self.request.get('auth') | |
| 97 if not auth: | |
| 98 abort_auth('No auth in request.') | |
| 99 return | |
| 100 | |
| 101 # Don't include the auth hmac itself in the check. | |
| 102 params = self.request.params.copy() | |
| 103 params.pop('auth') | |
| 104 blob = json.dumps(.params, ensure_ascii=True, sort_keys=True, | |
| 105 separators=('\0', '=')) | |
|
iannucci
2013/10/03 22:20:51
no! normal separators! blargh!
agable
2013/10/04 21:10:15
Done.
| |
| 106 check = hmac.new(key, blob, hashlib.sha256).hexdigest() | |
|
iannucci
2013/10/03 22:20:51
hmac should really return a file-like object which
| |
| 107 | |
| 108 if len(check) != len(auth): | |
|
iannucci
2013/10/03 22:20:51
you can do this check before the dumps since you k
agable
2013/10/04 21:10:15
Done.
| |
| 109 abort_auth('Incorrect authentication (length mismatch).') | |
| 110 return | |
| 111 | |
| 112 # Constant time comparison. | |
| 113 if reduce(lambda x,y: x or y, | |
|
iannucci
2013/10/03 22:20:51
I think you want | here not or, since you don't wa
Vadim Sh.
2013/10/04 04:38:19
I see you like functional languages a lot, Aaron :
agable
2013/10/04 21:10:15
Done.
agable
2013/10/04 21:10:15
I do! And I disagree -- this is exactly explicitly
| |
| 114 (ord(a) ^ ord(b) for a, b in zip(check, auth)), 0): | |
| 115 abort_auth('Incorrect authentication.') | |
| 116 return | |
| 117 | |
| 118 # Hooray, they made it! | |
| 119 setattr(self.request, 'hmac_authenticated', True) | |
| 120 handler(self, *args, **kwargs) | |
| 121 | |
| 122 return wrapper | |
| 123 return decorator | |
| 124 | |
| 125 | |
| 126 def CreateRequest(**params): | |
| 127 """Given a payload to send, constructs an authenticated request. | |
| 128 | |
| 129 Returns a dictionary containing: | |
| 130 id: Unique ID of this app, from the datastore AuthToken 'self' | |
| 131 time: Current Unix epoch time | |
| 132 auth: The hmac(key, id+time+parms, sha256), to authenticate the request, | |
| 133 where the key is the corresponding token in the app's AuthToken | |
| 134 **params: All of the GET/POST parameters | |
| 135 | |
| 136 It is up to the calling code to convert this dictionary into valid GET/POST | |
| 137 parameters. | |
| 138 """ | |
| 139 authtoken = ndb.Key(AuthToken, 'self').get() | |
| 140 if not authtoken: | |
| 141 raise AuthError('No AuthToken found for this app.') | |
| 142 id = authtoken.id | |
| 143 key = authtoken.token | |
| 144 | |
| 145 time = time.mktime(datetime.datetime.now().timetuple()) | |
|
Vadim Sh.
2013/10/04 04:38:19
I think code that generates HMAC for a request, an
agable
2013/10/04 21:10:15
Done.
| |
| 146 | |
| 147 ret = params.copy() | |
| 148 ret.update({'id': id, 'time': time}) | |
| 149 | |
| 150 blob = json.dumps(ret, ensure_ascii=True, sort_keys=True, | |
| 151 separators=('\0'. '=')) | |
|
iannucci
2013/10/03 22:20:51
I think this should be a comma, not a dot
also no
agable
2013/10/04 21:10:15
Already Done.
| |
| 152 auth = hmac.new(key, blob, hashlib.sha256).hexdigest() | |
| 153 | |
| 154 ret['auth'] = auth | |
| 155 return ret | |
| OLD | NEW |