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 |