Index: chromium-committers/auth_util.py |
=================================================================== |
--- chromium-committers/auth_util.py (revision 0) |
+++ chromium-committers/auth_util.py (revision 0) |
@@ -0,0 +1,106 @@ |
+# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Utilities for talking to another appengine app with authentication.""" |
+ |
+__author__ = 'agable@google.com (Aaron Gable)' |
+ |
+ |
+import datetime |
+import hashlib |
+import hmac |
+import time |
+ |
+from google.appengine.ext import ndb |
+ |
+ |
+class AuthError(Exception): |
+ pass |
+ |
+ |
+class AuthToken(ndb.Model): |
+ """Represents an id/key pair for authentication. |
+ |
+ This app also uses the id field as the id/name (unique datastore key) of the |
+ object for easy lookup. The app's own AuthToken is stored with name 'self'. |
+ |
+ Attributes: |
+ id: The unique identity for this token. |
+ token: The corresponding authentication key. |
+ """ |
+ id = ndb.StringProperty() |
+ token = ndb.StringProperty() |
+ |
+ |
+def CheckRequest(request): |
iannucci
2013/10/03 03:05:02
I would make this a decorator. Or a handler to sub
agable
2013/10/03 15:26:40
Then it couldn't be used the way it is in Chromium
agable
2013/10/03 19:59:03
Ok, decoratorized.
|
+ """Given an https request, confirms that it is correctly authenticated. |
+ |
+ Expects the request to contain: |
+ id: Unique ID of requester, used to get an AuthToken from ndb |
+ time: Unix epoch time the request was made (to prevent replay attacks) |
+ auth: The hmac(key, id+time+params, sha256), to authenticate the request, |
+ where the key is the corresponding token in the ID's AuthToken |
+ **params: All of the request GET/POST parameters |
+ |
+ Throws AuthError if things don't check out right. |
+ """ |
+ id = request.get('id') |
+ if not id: |
+ raise AuthError('No id in request.') |
+ authtoken = ndb.Key(AuthToken, id).get() |
+ if not authtoken: |
+ raise AuthError('No auth token found for id %s.' % id) |
+ key = authtoken.token |
+ |
+ time = request.get('time') |
+ if not time: |
+ raise AuthError('No time in request.') |
+ then = datetime.datetime.fromtimestamp(time) |
+ now = datetime.datetime.now() |
+ if abs(now - then) > datetime.timedelta(minutes=10): |
iannucci
2013/10/03 03:05:02
I would make this 10 min a configuration option
|
+ raise AuthError('Time more than 10 minutes out of sync.') |
+ |
+ auth = request.get('auth') |
+ if not auth: |
+ raise AuthError('No auth in request.') |
+ |
+ check = hmac.new(key, digestmod=hashlib.sha256) |
+ for param, value in sorted(request.params.items()): |
+ # Don't include the auth hmac itself in the check. |
+ if param == 'auth': |
+ continue |
+ check.update('%s=%s' % (param, value)) |
+ |
+ if check.hexdigest() != auth: |
iannucci
2013/10/03 03:05:02
Need to do a constant-time comparison here. Assert
agable
2013/10/03 19:59:03
Done.
|
+ raise AuthError('Incorrect authentication.') |
+ |
+ |
+def CreateRequest(**params): |
iannucci
2013/10/03 03:05:02
decorator.
Actually... when would this ever be us
agable
2013/10/03 15:26:40
Uh, this is exactly what you, Vadim, and I were di
iannucci
2013/10/03 22:20:51
Right, so the appengine code would never really ca
|
+ """Given a payload to send, constructs an authenticated request. |
+ |
+ Returns a dictionary containing: |
+ id: Unique ID of this app, from the datastore AuthToken 'self' |
+ time: Current Unix epoch time |
+ auth: The hmac(key, id+time+parms, sha256), to authenticate the request, |
+ where the key is the corresponding token in the app's AuthToken |
+ **params: All of the GET/POST parameters |
+ |
+ It is up to the calling code to convert this dictionary into valid GET/POST |
+ parameters. |
+ """ |
+ authtoken = ndb.Key(AuthToken, 'self').get() |
+ if not authtoken: |
+ raise AuthError('No AuthToken found for this app.') |
+ id = authtoken.id |
+ key = authtoken.token |
+ |
+ time = time.mktime(datetime.datetime.now().timetuple()) |
+ |
+ auth = hmac.new(key, digestmod=hashlib.sha256) |
+ for param, value in sorted(params.items()): |
+ auth.update('%s=%s' % (param, value)) |
Vadim Sh.
2013/10/03 17:04:53
you need to hmac(time + id + all other stuff), and
agable
2013/10/03 17:46:30
That's exactly what .update() does.
auth.update(a)
agable
2013/10/03 17:47:13
Oh whoops I see what you mean. Fixing.
|
+ |
+ ret = params.copy() |
+ ret.update({'id': id, 'time': time, 'auth': auth.hexdigest()}) |
+ return ret |