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 collections | |
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): | |
24 """Represents an id/key pair for authentication. | |
25 | |
26 Attributes: | |
27 client_name: 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_name = ndb.StringProperty() | |
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 for obj in hmac_params.iteritems(): | |
48 assert isinstance(obj, collections.Hashable) | |
iannucci
2013/10/09 23:06:49
assert all(isinstance(i, collections.Hashable) for
agable
2013/10/10 00:19:37
Done.
| |
49 blob = urllib.urlencode(sorted(hmac_params.items())) | |
50 logging.debug('Generating HMAC from blob: %s' % blob) | |
51 return hmac.new(authtoken.secret, blob, hashlib.sha256).hexdigest() | |
52 | |
53 | |
54 def CheckHmacAuth(handler): | |
55 """Decorator for webapp2 request handler methods. | |
56 | |
57 Only use on webapp2.RequestHandler methods (e.g. get, post, put). | |
58 | |
59 Expects the handler's self.request to contain: | |
60 id: Unique ID of requester, used to get an AuthToken from ndb | |
61 t: Unix epoch time the request was made (to prevent replay attacks) | |
62 auth: The hmac(key, id+time+params, sha256).hexdigest, to authenticate | |
63 the request, where the key is the matching token in the ID's AuthToken | |
64 **params: All of the request GET/POST parameters | |
65 | |
66 Sets request.authenticated to 'hmac' if successful. Otherwise, None. | |
67 """ | |
68 @functools.wraps(handler) | |
69 def wrapper(self, *args, **kwargs): | |
70 """Does the real legwork and calls the wrapped handler.""" | |
71 def abort_auth(log_msg): | |
72 """Helper method to be an exit hatch when authentication fails.""" | |
73 logging.warning(log_msg) | |
74 self.request.authenticated = None | |
75 handler(self, *args, **kwargs) | |
76 | |
77 def finish_auth(log_msg): | |
78 """Helper method to be an exit hatch when authentication succeeds.""" | |
79 logging.info(log_msg) | |
80 handler(self, *args, **kwargs) | |
81 | |
82 if getattr(self.request, 'authenticated', None): | |
83 finish_auth('Already authenticated.') | |
84 return | |
85 | |
86 # Get the id, time, and auth fields from the request. | |
87 client_id = self.request.get('id') | |
88 if not client_id: | |
89 abort_auth('No id in request.') | |
90 return | |
91 logging.debug('Request contained id: %s' % client_id) | |
92 authtoken = AuthToken.query(AuthToken.client_id == client_id).get() | |
93 if not authtoken: | |
94 abort_auth('No auth token stored for client.') | |
95 return | |
96 logging.debug('AuthToken is from client: %s' % authtoken.client) | |
97 | |
98 then = int(self.request.get('t', '0')) | |
99 if not then: | |
100 abort_auth('No timestamp in request.') | |
101 return | |
102 logging.debug('Request generated at time: %s' % then) | |
103 now = int(time.time()) | |
104 if abs(now - then) > 60: | |
105 abort_auth('Timestamp too far off, token expired.') | |
106 return | |
107 | |
108 auth = self.request.get('auth') | |
109 if not auth: | |
110 abort_auth('No auth in request.') | |
111 return | |
112 logging.debug('Request contained auth hash: %s' % auth) | |
113 | |
114 # Don't include the auth hmac itself in the check. | |
115 params = self.request.params.copy() | |
116 params.pop('auth') | |
117 check = GenerateHmac(authtoken, **params) | |
118 logging.debug('Expected auth hash is: %s' % check) | |
119 | |
120 # Constant time comparison. | |
121 if len(auth) != len(check): | |
122 abort_auth('Incorrect authentication (length mismatch).') | |
iannucci
2013/10/09 23:06:49
I don't see why the exact length of the hash is te
| |
123 return | |
124 if reduce(operator.or_, | |
125 (ord(a) ^ ord(b) for a, b in zip(check, auth)), 0): | |
126 abort_auth('Incorrect authentication.') | |
127 return | |
128 | |
129 # Hooray, they made it! | |
130 self.request.authenticated = 'hmac' | |
131 handler(self, *args, **kwargs) | |
132 | |
133 return wrapper | |
134 | |
135 | |
136 def CreateRequest(**params): | |
137 """Given a payload to send, constructs an authenticated request. | |
138 | |
139 Returns a dictionary containing: | |
140 id: Unique ID of this app, from the datastore AuthToken 'self' | |
141 t: Current Unix epoch time | |
142 auth: The hmac(key, id+time+parms, sha256), to authenticate the request, | |
143 where the key is the corresponding secret in the app's AuthToken | |
144 **params: All of the GET/POST parameters | |
145 | |
146 It is up to the calling code to convert this dictionary into valid GET/POST | |
147 parameters. | |
148 """ | |
149 authtoken = ndb.Key(AuthToken, 'self').get() | |
150 if not authtoken: | |
151 raise AuthError('No AuthToken found for this app.') | |
152 | |
153 now = str(int(time.time())) | |
154 | |
155 ret = params.copy() | |
156 ret.update({'id': authtoken.client_id, 't': now, | |
157 'auth': GenerateHmac(authtoken, t=now, **params)}) | |
158 return ret | |
159 | |
160 | |
161 class AuthError(Exception): | |
162 pass | |
163 | |
164 | |
165 # There needs to be one AuthToken in the datastore so it can be added or edited | |
166 # from the admin console. Do this one-time setup when this module is imported. | |
167 if not ndb.Key(AuthToken, 'self').get(): | |
168 AuthToken(key=ndb.Key(AuthToken, 'self')).put() | |
OLD | NEW |