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 generating and verifying hmac 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 operator | |
| 17 import time | |
| 18 import urllib | |
| 19 | |
| 20 from google.appengine.ext import ndb | |
| 21 | |
| 22 | |
| 23 class AuthToken(ndb.Model): | |
|
Vadim Sh.
2013/10/09 19:13:02
Can |client| or |client_id| be reused as a key of
agable
2013/10/09 19:23:51
I'd like to reuse client_id as the key of the mode
| |
| 24 """Represents an id/key pair for authentication. | |
| 25 | |
| 26 Attributes: | |
| 27 client: The human-readable name of the client. | |
| 28 client_id: The unique identity for the client that uses this token. | |
| 29 secret: The corresponding authentication key. | |
| 30 """ | |
| 31 client = ndb.StringProperty() | |
|
iannucci
2013/10/09 20:34:55
'name' ? 'client_name' ?
agable
2013/10/09 22:36:04
'name' is a reserved keyword. will do client_name.
| |
| 32 client_id = ndb.StringProperty() | |
| 33 secret = ndb.StringProperty() | |
| 34 | |
| 35 | |
| 36 def GenerateHmac(authtoken, t=None, **params): | |
| 37 """Generates an HMAC cryptographic hash of the given parameters. | |
| 38 | |
| 39 Can be used either both for generating outgoing authentication and for | |
| 40 validating incoming requests. Automatically included the authtoken's client_id | |
| 41 and the time in the hashed parameter blob. If t (timestamp) is None, uses now. | |
| 42 """ | |
| 43 if t is None: | |
| 44 t = str(int(time.time())) | |
| 45 hmac_params = params.copy() | |
| 46 hmac_params.update({'id': authtoken.client_id, 't': t}) | |
| 47 blob = urllib.urlencode(sorted(hmac_params.items())) | |
|
iannucci
2013/10/09 20:34:55
I'm assuming this will throw if some of the parame
agable
2013/10/09 22:36:04
I assumed so, but apparently not D:
| |
| 48 logging.debug('Generating HMAC from blob: %s' % blob) | |
| 49 return hmac.new(authtoken.secret, blob, hashlib.sha256).hexdigest() | |
| 50 | |
| 51 | |
| 52 def CheckHmacAuth(handler): | |
| 53 """Decorator for webapp2 request handler methods. | |
| 54 | |
| 55 Only use on webapp2.RequestHandler methods (e.g. get, post, put). | |
| 56 | |
| 57 Expects the handler's self.request to contain: | |
| 58 id: Unique ID of requester, used to get an AuthToken from ndb | |
| 59 t: Unix epoch time the request was made (to prevent replay attacks) | |
| 60 auth: The hmac(key, id+time+params, sha256).hexdigest, to authenticate | |
| 61 the request, where the key is the matching token in the ID's AuthToken | |
| 62 **params: All of the request GET/POST parameters | |
| 63 | |
| 64 Sets request.authenticated to 'hmac' if successful. Otherwise, None. | |
| 65 """ | |
| 66 @functools.wraps(handler) | |
| 67 def wrapper(self, *args, **kwargs): | |
| 68 """Does the real legwork and calls the wrapped handler.""" | |
| 69 def abort_auth(log_msg): | |
| 70 """Helper method to be an exit hatch when authentication fails.""" | |
| 71 logging.warning(log_msg) | |
| 72 self.request.authenticated = None | |
| 73 handler(self, *args, **kwargs) | |
| 74 | |
| 75 def finish_auth(log_msg): | |
| 76 """Helper method to be an exit hatch when authentication succeeds.""" | |
| 77 logging.info(log_msg) | |
| 78 handler(self, *args, **kwargs) | |
| 79 | |
| 80 if getattr(self.request, 'authenticated', None): | |
| 81 finish_auth('Already authenticated.') | |
| 82 return | |
| 83 | |
| 84 # Get the id, time, and auth fields from the request. | |
| 85 client_id = self.request.get('id') | |
| 86 if not client_id: | |
| 87 abort_auth('No id in request.') | |
| 88 return | |
| 89 logging.debug('Request contained id: %s' % client_id) | |
| 90 authtoken = AuthToken.query(AuthToken.client_id == client_id).get() | |
| 91 if not authtoken: | |
| 92 abort_auth('No auth token stored for client.') | |
| 93 return | |
| 94 logging.debug('AuthToken is from client: %s' % authtoken.client) | |
| 95 | |
| 96 then = int(self.request.get('t', '0')) | |
| 97 if not then: | |
| 98 abort_auth('No timestamp in request.') | |
| 99 return | |
| 100 logging.debug('Request generated at time: %s' % then) | |
| 101 now = int(time.time()) | |
| 102 if (datetime.timedelta(seconds=abs(now - then)) > | |
|
Vadim Sh.
2013/10/09 19:13:02
nit: abs(now - then) > 60, it's shorter.
Also chan
agable
2013/10/09 19:23:51
Done in patchset 6.
| |
| 103 datetime.timedelta(minutes=1)): | |
|
iannucci
2013/10/09 20:34:55
minutes=1? 10?
Should this be at least a GLOBAL i
agable
2013/10/09 22:36:04
In general I'm opposed to GLOBALS except for where
| |
| 104 abort_auth('Time more than 10 minutes out of sync.') | |
|
iannucci
2013/10/09 20:34:55
message should match actual interval
agable
2013/10/09 22:36:04
Addressed vadim's comment.
| |
| 105 return | |
| 106 | |
| 107 auth = self.request.get('auth') | |
| 108 if not auth: | |
| 109 abort_auth('No auth in request.') | |
| 110 return | |
| 111 logging.debug('Request contained auth hash: %s' % auth) | |
| 112 if len(auth) != 64: # 256 bits / 4 bits per hexadecimal char | |
|
iannucci
2013/10/09 20:34:55
Instead of comment, maybe:
HEX_CHARS_PER_BYTE = 2
agable
2013/10/09 22:36:04
See response to your comment about the expiration
| |
| 113 abort_auth('Incorrect authentication (length mismatch).') | |
| 114 return | |
| 115 | |
|
iannucci
2013/10/09 20:34:55
I would additionally do binascii.unhexlify(auth),
agable
2013/10/09 22:36:04
Interesting. I guess I don't see the benefit? Espe
| |
| 116 # Don't include the auth hmac itself in the check. | |
| 117 params = self.request.params.copy() | |
| 118 params.pop('auth') | |
| 119 check = GenerateHmac(authtoken, **params) | |
| 120 logging.debug('Expected auth hash is: %s' % check) | |
| 121 | |
| 122 # Constant time comparison. | |
| 123 if reduce(operator.or_, | |
| 124 (ord(a) ^ ord(b) for a, b in zip(check, auth)), 0): | |
| 125 abort_auth('Incorrect authentication.') | |
| 126 return | |
| 127 | |
| 128 # Hooray, they made it! | |
| 129 self.request.authenticated = 'hmac' | |
| 130 handler(self, *args, **kwargs) | |
| 131 | |
| 132 return wrapper | |
| 133 | |
| 134 | |
| 135 def CreateRequest(**params): | |
| 136 """Given a payload to send, constructs an authenticated request. | |
| 137 | |
| 138 Returns a dictionary containing: | |
| 139 id: Unique ID of this app, from the datastore AuthToken 'self' | |
| 140 t: Current Unix epoch time | |
| 141 auth: The hmac(key, id+time+parms, sha256), to authenticate the request, | |
| 142 where the key is the corresponding secret in the app's AuthToken | |
| 143 **params: All of the GET/POST parameters | |
| 144 | |
| 145 It is up to the calling code to convert this dictionary into valid GET/POST | |
| 146 parameters. | |
| 147 """ | |
| 148 authtoken = ndb.Key(AuthToken, 'self').get() | |
| 149 if not authtoken: | |
| 150 raise AuthError('No AuthToken found for this app.') | |
| 151 | |
| 152 now = str(int(time.time())) | |
| 153 | |
| 154 ret = params.copy() | |
| 155 ret.update({'id': authtoken.client_id, 't': now, | |
| 156 'auth': GenerateHmac(authtoken, t=now, **params)}) | |
| 157 return ret | |
| 158 | |
| 159 | |
| 160 class AuthError(Exception): | |
| 161 pass | |
| 162 | |
| 163 | |
| 164 # There needs to be one AuthToken in the datastore so it can be added or edited | |
| 165 # from the admin console. Do this one-time setup when this module is imported. | |
| 166 if not ndb.Key(AuthToken, 'self').get(): | |
|
Vadim Sh.
2013/10/09 19:13:02
Are you sure ndb magic works outside of request ha
agable
2013/10/09 19:23:51
Yep, it works (have confirmed via dev_appserver as
| |
| 167 AuthToken(key=ndb.Key(AuthToken, 'self')).put() | |
|
iannucci
2013/10/09 20:34:55
Ew. Maybe just do this with a _ah/warmup handler?
agable
2013/10/09 22:36:04
Tried that. Doesn't work. /_ah/warmup is only call
| |
| OLD | NEW |