Chromium Code Reviews| Index: chromium-committers/auth_util.py |
| =================================================================== |
| --- chromium-committers/auth_util.py (revision 0) |
| +++ chromium-committers/auth_util.py (revision 0) |
| @@ -0,0 +1,155 @@ |
| +# 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 functools |
| +import hmac |
| +import json |
| +import logging |
| +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 CheckHmacAuth(should_403=True): |
|
Vadim Sh.
2013/10/04 04:38:19
single default arg in such pseudo decorators (deco
agable
2013/10/04 21:10:15
I absolutely agree. I thought about this for a whi
|
| + """Decorator for webapp2 request handler methods. |
| + |
| + Only use on webapp2.RequestHandler methods (e.g. get, post, put). |
| + |
| + 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).hexdigest, to authenticate |
| + the request, where the key is the matching token in the ID's AuthToken |
| + **params: All of the request GET/POST parameters |
| + |
| + If should_403 is True, simply 403s if things don't check out right. |
| + Otherwise, sets request.hmac_authenticated to True or False, as appropriate. |
| + """ |
| + def decorator(handler): |
| + """The real decorator, conditioned on should_throw.""" |
|
Vadim Sh.
2013/10/04 04:38:19
should_403?
agable
2013/10/04 21:10:15
Done.
|
| + @functools.wrap(handler) |
|
Vadim Sh.
2013/10/04 04:38:19
wraps
agable
2013/10/04 21:10:15
Already done (I may have uploaded this patchset be
|
| + def wrapper(self, *args, **kwargs): |
| + """Does the real legwork and calls the wrapped handler or 403s.""" |
| + def abort_auth(log_msg): |
| + """Helper method to be an exit hatch when authentication fails. |
| + |
| + If should_403 is True, writes a 403 to the response. |
| + If should_403 is False, sets hmac_authenticated to False and falls |
| + through to the wrapped handler. |
| + """ |
| + logging.info(log_msg) |
| + if should_403: |
| + self.response.headers['Content-Type'] = 'text/plain' |
| + self.reponse.status = 403 |
| + self.response.write('403: Forbidden') |
|
Vadim Sh.
2013/10/04 04:38:19
Same code shorter and ensuring request processing
agable
2013/10/04 21:10:15
Thanks, didn't know about that! Done.
|
| + else: |
| + setattr(self.request, 'hmac_authenticated', False) |
|
iannucci
2013/10/03 22:20:51
self.request.hmac_authenticated = False
doesn't w
agable
2013/10/04 21:10:15
Done.
|
| + handler(self, *args, **kwargs) |
| + |
| + # Get the id, time, and auth fields from the request. |
| + id = self.request.get('id') |
| + if not id: |
| + abort_auth('No id in request.') |
|
Vadim Sh.
2013/10/04 04:38:19
I'd rename it into 'finish_auth' or something with
agable
2013/10/04 21:10:15
I'd say that it still clearly is an abort -- of th
|
| + return |
| + authtoken = ndb.Key(AuthToken, id).get() |
| + if not authtoken: |
| + abort_auth('No auth token found for id %s.' % id) |
| + return |
| + key = authtoken.token |
| + |
| + time = self.request.get('time') |
| + if not time: |
| + abort_auth('No time in request.') |
| + return |
| + then = datetime.datetime.fromtimestamp(time) |
| + now = datetime.datetime.now() |
| + if abs(now - then) > datetime.timedelta(minutes=10): |
|
Vadim Sh.
2013/10/04 04:38:19
for inter app engine communication 10 min is forev
agable
2013/10/04 21:10:15
Agreed. This isn't solely for inter-appengine comm
|
| + abort_auth('Time more than 10 minutes out of sync.') |
| + return |
| + |
| + auth = self.request.get('auth') |
| + if not auth: |
| + abort_auth('No auth in request.') |
| + return |
| + |
| + # Don't include the auth hmac itself in the check. |
| + params = self.request.params.copy() |
| + params.pop('auth') |
| + blob = json.dumps(.params, ensure_ascii=True, sort_keys=True, |
| + separators=('\0', '=')) |
|
iannucci
2013/10/03 22:20:51
no! normal separators! blargh!
agable
2013/10/04 21:10:15
Done.
|
| + check = hmac.new(key, blob, hashlib.sha256).hexdigest() |
|
iannucci
2013/10/03 22:20:51
hmac should really return a file-like object which
|
| + |
| + if len(check) != len(auth): |
|
iannucci
2013/10/03 22:20:51
you can do this check before the dumps since you k
agable
2013/10/04 21:10:15
Done.
|
| + abort_auth('Incorrect authentication (length mismatch).') |
| + return |
| + |
| + # Constant time comparison. |
| + if reduce(lambda x,y: x or y, |
|
iannucci
2013/10/03 22:20:51
I think you want | here not or, since you don't wa
Vadim Sh.
2013/10/04 04:38:19
I see you like functional languages a lot, Aaron :
agable
2013/10/04 21:10:15
Done.
agable
2013/10/04 21:10:15
I do! And I disagree -- this is exactly explicitly
|
| + (ord(a) ^ ord(b) for a, b in zip(check, auth)), 0): |
| + abort_auth('Incorrect authentication.') |
| + return |
| + |
| + # Hooray, they made it! |
| + setattr(self.request, 'hmac_authenticated', True) |
| + handler(self, *args, **kwargs) |
| + |
| + return wrapper |
| + return decorator |
| + |
| + |
| +def CreateRequest(**params): |
| + """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()) |
|
Vadim Sh.
2013/10/04 04:38:19
I think code that generates HMAC for a request, an
agable
2013/10/04 21:10:15
Done.
|
| + |
| + ret = params.copy() |
| + ret.update({'id': id, 'time': time}) |
| + |
| + blob = json.dumps(ret, ensure_ascii=True, sort_keys=True, |
| + separators=('\0'. '=')) |
|
iannucci
2013/10/03 22:20:51
I think this should be a comma, not a dot
also no
agable
2013/10/04 21:10:15
Already Done.
|
| + auth = hmac.new(key, blob, hashlib.sha256).hexdigest() |
| + |
| + ret['auth'] = auth |
| + return ret |