def parse_token(self, token, now=None): """Extract the data embedded in the given token, if valid. The token is valid if it has a valid signature and if the embedded expiry time has not passed. If the token is not valid then this method raises ValueError. """ # Parse the payload and signature from the token. try: decoded_token = b64decode(token.encode("ascii")) except TypeError as e: raise ValueError(str(e)) # pragma: nocover payload = decoded_token[:-self.hashmod_digest_size] sig = decoded_token[-self.hashmod_digest_size:] # Carefully check the signature. # This is a deliberately slow string-compare to avoid timing attacks. # Read the docstring of strings_differ for more details. expected_sig = self._get_signature(payload) if strings_differ(sig, expected_sig): raise ValueError("token has invalid signature") # Only decode *after* we've confirmed the signature. data = json.loads(payload.decode("utf8")) # Check whether it has expired. if now is None: now = time.time() if data["expires"] <= now: raise ValueError("token has expired") return data
def parse_token(self, token, now=None): """Extract the data embedded in the given token, if valid. The token is valid if it has a valid signature and if the embedded expiry time has not passed. If the token is not valid then this method raises ValueError. """ # Parse the payload and signature from the token. try: decoded_token = decode_token_bytes(token) except (TypeError, ValueError) as e: raise errors.MalformedTokenError(str(e)) payload = decoded_token[:-self.hashmod_digest_size] sig = decoded_token[-self.hashmod_digest_size:] # Carefully check the signature. # This is a deliberately slow string-compare to avoid timing attacks. # Read the docstring of strings_differ for more details. expected_sig = self._get_signature(payload) if strings_differ(sig, expected_sig): raise errors.InvalidSignatureError() # Only decode *after* we've confirmed the signature. # This should never fail, but well, you can't be too careful. try: data = json.loads(payload.decode("utf8")) except ValueError as e: # pragma: nocover raise errors.MalformedTokenError(str(e)) # Check whether it has expired. if now is None: now = time.time() if data["expires"] <= now: raise errors.ExpiredTokenError() return data
def test_strings_differ(self): # We can't really test the timing-invariance, but # we can test that we actually compute equality! self.assertTrue(strings_differ(b"", b"a")) self.assertTrue(strings_differ(b"b", b"a")) self.assertTrue(strings_differ(b"cc", b"a")) self.assertTrue(strings_differ(b"cc", b"aa")) self.assertFalse(strings_differ(b"", b"")) self.assertFalse(strings_differ(b"D", b"D")) self.assertFalse(strings_differ(b"EEE", b"EEE"))
class TokenManager(object): """Class for managing signed authentication tokens. This class provides a generic facility for creating and verifying signed authentication tokens. It's useful as a basis for token-based auth schemes such as Two-Legged OAuth or MAC Access Auth, and should provide a good balance between speed, memory usage and security for most applications. The TokenManager must be initialized with a "master secret" which is used to crytographically secure the tokens. Each token consists of a JSON object with an appended HMAC signature. Tokens also have a corresponding "token secret" generated using HKDF, which can be given to clients for use in signature-based authentication schemes. The constructor takes the following arguments: * secret: string key used for signing the token; if not specified then a random bytestring is used. * timeout: the time after which a token will expire. * hashmod: the hashing module to use for various HMAC operations; if not specified then hashlib.sha1 will be used. """ def __init__(self, secret=None, timeout=None, hashmod=None): if secret is None: secret = DEFAULT_SECRET if timeout is None: timeout = DEFAULT_TIMEOUT if hashmod is None: hashmod = DEFAULT_HASHMOD if isinstance(hashmod, basestring): hashmod = getattr(hashlib, hashmod) self.secret = secret self.timeout = timeout self.hashmod = hashmod self.hashmod_digest_size = hashmod().digest_size self._sig_secret = HKDF(self.secret, salt=None, info="SIGNING", size=self.hashmod_digest_size) def make_token(self, data): """Generate a new token embedding the given dict of data. The token is a JSON dump of the given data along with an expiry time and salt. It has a HMAC signature appended and is b64-encoded for transmission. """ data = data.copy() if "salt" not in data: data["salt"] = os.urandom(3).encode("hex") if "expires" not in data: data["expires"] = time.time() + self.timeout payload = json.dumps(data) sig = self._get_signature(payload) assert len(sig) == self.hashmod_digest_size return b64encode(payload + sig) def parse_token(self, token, now=None): """Extract the data embedded in the given token, if valid. The token is valid is it has a valid signature and if the embedded expiry time has not passed. If the token is not valid then this method raises ValueError. """ # Parse the payload and signature from the token. try: decoded_token = b64decode(token) except TypeError, e: raise ValueError(str(e)) payload = decoded_token[:-self.hashmod_digest_size] sig = decoded_token[-self.hashmod_digest_size:] # Carefully check the signature. # This is a deliberately slow string-compare to avoid timing attacks. # Read the docstring of strings_differ for more details. expected_sig = self._get_signature(payload) if strings_differ(sig, expected_sig): raise ValueError("token has invalid signature") # Only decode *after* we've confirmed the signature. data = json.loads(payload) # Check whether it has expired. if now is None: now = time.time() if data["expires"] <= now: raise ValueError("token has expired") return data