Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(106)

Side by Side Diff: chromium-committers/hmac_util.py

Issue 25515004: Add chromium-committers appengine app. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/
Patch Set: Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chromium-committers/constants.py ('k') | chromium-committers/model.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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
OLDNEW
« no previous file with comments | « chromium-committers/constants.py ('k') | chromium-committers/model.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698