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 assert all(isinstance(obj, collections.Hashable) |
| 48 for obj in hmac.params.iteritems()) |
| 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).') |
| 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 |