def test_convenience_functions(self): token = tokenlib.make_token({"hello": "world"}) self.assertEquals(tokenlib.parse_token(token)["hello"], "world") self.assertRaises(ValueError, tokenlib.parse_token, token, secret="X") self.assertEquals(tokenlib.get_derived_secret(token), tokenlib.get_derived_secret(token)) self.assertNotEquals(tokenlib.get_derived_secret(token), tokenlib.get_derived_secret(token, secret="X"))
def decode_hawk_id(self, request, tokenid): """Decode a Hawk token id into its userid and secret key. This method determines the appropriate secrets to use for the given request, then passes them on to tokenlib to handle the given Hawk token. If the id is invalid then ValueError will be raised. """ # There might be multiple secrets in use, if we're in the # process of transitioning from one to another. Try each # until we find one that works. node_name = self._get_node_name(request) secrets = self._get_token_secrets(node_name) for secret in secrets: try: data = tokenlib.parse_token(tokenid, secret=secret) userid = data["uid"] token_node_name = data["node"] if token_node_name != node_name: raise ValueError("incorrect node for this token") key = tokenlib.get_derived_secret(tokenid, secret=secret) break except (ValueError, KeyError): pass else: logger.warn("Authentication Failed: invalid hawk id") raise ValueError("invalid Hawk id") return userid, key
def delete_service_data(config, service, user, timeout=60, settings=None): """Send a data-deletion request to the user's service node. This is a little bit of hackery to cause the user's service node to remove any data it still has stored for the user. We simulate a DELETE request from the user's own account. """ secrets = config.registry.settings['tokenserver.secrets'] pattern = config.registry['endpoints_patterns'][service] node_secrets = secrets.get(user.node) if not node_secrets: msg = "The node %r does not have any shared secret" % (user.node, ) raise ValueError(msg) token = tokenlib.make_token( { "uid": user.uid, "node": user.node, "fxa_uid": user.email.split("@", 1)[0], "fxa_kid": format_key_id(user.keys_changed_at or user.generation, user.client_state.decode('hex')), }, secret=node_secrets[-1]) secret = tokenlib.get_derived_secret(token, secret=node_secrets[-1]) endpoint = pattern.format(uid=user.uid, service=service, node=user.node) auth = HawkAuth(token, secret) if settings and settings.dryrun: return resp = requests.delete(endpoint, auth=auth, timeout=timeout) if resp.status_code >= 400 and resp.status_code != 404: resp.raise_for_status()
def delete_service_data(user, secret, timeout=60): """Send a data-deletion request to the user's service node. This is a little bit of hackery to cause the user's service node to remove any data it still has stored for the user. We simulate a DELETE request from the user's own account. """ token = tokenlib.make_token( { "uid": user.uid, "node": user.node, "fxa_uid": user.email.split("@", 1)[0], "fxa_kid": format_key_id(user.keys_changed_at or user.generation, binascii.unhexlify(user.client_state)), }, secret=secret) secret = tokenlib.get_derived_secret(token, secret=secret) endpoint = PATTERN.format(uid=user.uid, node=user.node) auth = HawkAuth(token, secret) resp = requests.delete(endpoint, auth=auth, timeout=timeout) if resp.status_code >= 400 and resp.status_code != 404: resp.raise_for_status()
def delete_service_data(config, service, user, timeout=60): """Send a data-deletion request to the user's service node. This is a little bit of hackery to cause the user's service node to remove any data it still has stored for the user. We simulate a DELETE request from the user's own account. """ secrets = config.registry.settings['tokenserver.secrets'] pattern = config.registry['endpoints_patterns'][service] node_secrets = secrets.get(user.node) if not node_secrets: msg = "The node %r does not have any shared secret" % (user.node,) raise ValueError(msg) token = tokenlib.make_token({ "uid": user.uid, "node": user.node, "fxa_uid": user.email.split("@", 1)[0], "fxa_kid": user.client_state }, secret=node_secrets[-1]) secret = tokenlib.get_derived_secret(token, secret=node_secrets[-1]) endpoint = pattern.format(uid=user.uid, service=service, node=user.node) auth = HawkAuth(token, secret) resp = requests.delete(endpoint, auth=auth, timeout=timeout) if resp.status_code >= 400 and resp.status_code != 404: resp.raise_for_status()
def test_purging_of_old_user_records(self): # Make some old user records. service = "sync-1.1" email = "*****@*****.**" user = self.backend.allocate_user(service, email, client_state="aa", generation=123) self.backend.update_user(service, user, client_state="bb", generation=456, keys_changed_at=450) self.backend.update_user(service, user, client_state="cc", generation=789) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 3) user = self.backend.get_user(service, email) self.assertEquals(user["client_state"], "cc") self.assertEquals(len(user["old_client_states"]), 2) # The default grace-period should prevent any cleanup. self.assertTrue(purge_old_records(self.ini_file)) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 3) self.assertEqual(len(self.service_requests), 0) # With no grace period, we should cleanup two old records. self.assertTrue(purge_old_records(self.ini_file, grace_period=0)) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 1) self.assertEqual(len(self.service_requests), 2) # Check that the proper delete requests were made to the service. secrets = self.config.registry.settings["tokenserver.secrets"] node_secret = secrets.get(self.service_node)[-1] expected_kids = ["0000000000450-uw", "0000000000123-qg"] for i, environ in enumerate(self.service_requests): # They must be to the correct path. self.assertEquals(environ["REQUEST_METHOD"], "DELETE") self.assertTrue(re.match("/1.1/[0-9]+", environ["PATH_INFO"])) # They must have a correct request signature. token = hawkauthlib.get_id(environ) secret = tokenlib.get_derived_secret(token, secret=node_secret) self.assertTrue(hawkauthlib.check_signature(environ, secret)) userdata = tokenlib.parse_token(token, secret=node_secret) self.assertTrue("uid" in userdata) self.assertTrue("node" in userdata) self.assertEqual(userdata["fxa_uid"], "test") self.assertEqual(userdata["fxa_kid"], expected_kids[i]) # Check that the user's current state is unaffected user = self.backend.get_user(service, email) self.assertEquals(user["client_state"], "cc") self.assertEquals(len(user["old_client_states"]), 0)
def _generate_token_credentials(self): """Pick an identity, log in and generate the auth token.""" # If the server_url has a hash fragment, it's a storage node and # that's the secret. Otherwise it's a token server url. uid = random.randint(1, 1000000) url = urlparse(self.server_url) if url.fragment: endpoint = url._replace(fragment="", path="/1.5/" + str(uid)) self.endpoint_url = urlunparse(endpoint) data = { "uid": uid, "node": urlunparse(url._replace(fragment="")), "expires": time.time() + ASSERTION_LIFETIME, } self.auth_token = tokenlib.make_token(data, secret=url.fragment) self.auth_secret = tokenlib.get_derived_secret(self.auth_token, secret=url.fragment) else: email = "user%s@%s" % (uid, MOCKMYID_DOMAIN) exp = time.time() + ASSERTION_LIFETIME + HawkAuth.timeskew assertion = browserid.tests.support.make_assertion( email=email, audience=self.server_url, issuer=MOCKMYID_DOMAIN, issuer_keypair=(None, MOCKMYID_PRIVATE_KEY), exp=int(exp * 1000), ) token_url = self.server_url + "/1.0/sync/1.5" response = self.session.get(token_url, headers={ "Authorization": "BrowserID " + assertion, }) # Maybe timeskew between client and server? if response.status_code == 401: server_time = int(response.headers["X-Timestamp"]) HawkAuth.timeskew = server_time - int(time.time()) exp = time.time() + ASSERTION_LIFETIME + HawkAuth.timeskew assertion = browserid.tests.support.make_assertion( email=email, audience=self.server_url, issuer=MOCKMYID_DOMAIN, issuer_keypair=(None, MOCKMYID_PRIVATE_KEY), exp=int(exp * 1000), ) response = self.session.get(token_url, headers={ "Authorization": "BrowserID " + assertion, }) response.raise_for_status() credentials = response.json() self.auth_token = credentials["id"].encode('ascii') self.auth_secret = credentials["key"].encode('ascii') self.endpoint_url = credentials["api_endpoint"] return self.auth_token, self.auth_secret, self.endpoint_url
def test_valid_oauth_request(self): oauth_token = self.oauth_token headers = { 'Authorization': 'Bearer %s' % oauth_token, 'X-KeyID': '1234-qqo' } # Send a valid request, allocating a new user res = self.app.get('/1.0/sync/1.5', headers=headers) fxa_uid = self.session.uid # Retrieve the user from the database user = self._get_user(res.json['uid']) # First, let's verify that the token we received is valid. To do this, # we can unpack the hawk header ID into the payload and its signature # and then construct a tokenlib token to compute the signature # ourselves. To obtain a matching signature, we use the same secret as # is used by Tokenserver. raw = urlsafe_b64decode(res.json['id']) payload = raw[:-32] signature = raw[-32:] payload_dict = json.loads(payload.decode('utf-8')) # The `id` payload should include a field indicating the origin of the # token self.assertEqual(payload_dict['tokenserver_origin'], 'rust') signing_secret = self.TOKEN_SIGNING_SECRET expected_token = tokenlib.make_token(payload_dict, secret=signing_secret) expected_signature = urlsafe_b64decode(expected_token)[-32:] # Using the #compare_digest method here is not strictly necessary, as # this is not a security-sensitive situation, but it's good practice self.assertTrue(hmac.compare_digest(expected_signature, signature)) # Check that the given key is a secret derived from the hawk ID expected_secret = tokenlib.get_derived_secret(res.json['id'], secret=signing_secret) self.assertEqual(res.json['key'], expected_secret) # Check to make sure the remainder of the fields are valid self.assertEqual(res.json['uid'], user['uid']) self.assertEqual(res.json['api_endpoint'], '%s/1.5/%s' % (self.NODE_URL, user['uid'])) self.assertEqual(res.json['duration'], DEFAULT_TOKEN_DURATION) self.assertEqual(res.json['hashalg'], 'sha256') self.assertEqual(res.json['hashed_fxa_uid'], self._fxa_metrics_hash(fxa_uid)[:32]) self.assertEqual(res.json['node_type'], 'spanner') # The response should have an X-Timestamp header that contains the # number of seconds since the UNIX epoch self.assertIn('X-Timestamp', res.headers) self.assertIsNotNone(int(res.headers['X-Timestamp'])) token = self.unsafelyParseToken(res.json['id']) self.assertIn('hashed_device_id', token) self.assertEqual(token["uid"], res.json["uid"]) self.assertEqual(token["fxa_uid"], fxa_uid) self.assertEqual(token["fxa_kid"], "0000000001234-qqo") self.assertNotEqual(token["hashed_fxa_uid"], token["fxa_uid"]) self.assertEqual(token["hashed_fxa_uid"], res.json["hashed_fxa_uid"]) self.assertIn("hashed_device_id", token)
def test_valid_browserid_request(self): assertion = self.browserid_assertion headers = { 'Authorization': 'BrowserID %s' % assertion, 'X-Client-State': 'aaaa' } # Send a valid request, allocating a new user res = self.app.get('/1.0/sync/1.5', headers=headers) fxa_uid = self.session.uid # Retrieve the user from the database user = self._get_user(res.json['uid']) # First, let's verify that the token we received is valid. To do this, # we can unpack the hawk header ID into the payload and its signature # and then construct a tokenlib token to compute the signature # ourselves. To obtain a matching signature, we use the same secret as # is used by Tokenserver. raw = urlsafe_b64decode(res.json['id']) payload = raw[:-32] signature = raw[-32:] payload_dict = json.loads(payload.decode('utf-8')) signing_secret = self.TOKEN_SIGNING_SECRET expected_token = tokenlib.make_token(payload_dict, secret=signing_secret) expected_signature = urlsafe_b64decode(expected_token)[-32:] # Using the #compare_digest method here is not strictly necessary, as # this is not a security-sensitive situation, but it's good practice self.assertTrue(hmac.compare_digest(expected_signature, signature)) # Check that the given key is a secret derived from the hawk ID expected_secret = tokenlib.get_derived_secret(res.json['id'], secret=signing_secret) self.assertEqual(res.json['key'], expected_secret) # Check to make sure the remainder of the fields are valid self.assertEqual(res.json['uid'], user['uid']) self.assertEqual(res.json['api_endpoint'], '%s/1.5/%s' % (self.NODE_URL, user['uid'])) self.assertEqual(res.json['duration'], DEFAULT_TOKEN_DURATION) self.assertEqual(res.json['hashalg'], 'sha256') self.assertEqual(res.json['hashed_fxa_uid'], self._fxa_metrics_hash(fxa_uid)[:32]) self.assertEqual(res.json['node_type'], 'spanner') token = self.unsafelyParseToken(res.json['id']) self.assertIn('hashed_device_id', token) self.assertEqual(token["uid"], res.json["uid"]) self.assertEqual(token["fxa_uid"], fxa_uid) assertion = self.browserid_assertion keys_changed_at = \ self._extract_keys_changed_at_from_assertion(assertion) self.assertEqual(token["fxa_kid"], "%s-qqo" % str(keys_changed_at)) self.assertNotEqual(token["hashed_fxa_uid"], token["fxa_uid"]) self.assertEqual(token["hashed_fxa_uid"], res.json["hashed_fxa_uid"]) self.assertIn("hashed_device_id", token)
def create_token(args): expires = int(time.time()) + args.duration token_data = { 'uid': args.uid, 'node': args.node, 'expires': expires, 'fxa_uid': args.fxa_uid, 'fxa_kid': args.fxa_kid, 'hashed_fxa_uid': metrics_hash(args, args.fxa_uid), 'hashed_device_id': metrics_hash(args, args.device_id), 'salt': SALT, } token = tokenlib.make_token(token_data, secret=args.secret) key = tokenlib.get_derived_secret(token, secret=args.secret) return token, key, expires, SALT
def encode_hawk_id( # pylint: disable=E0202, W0613 self, request, userid=None, **data): """Encode the given userid into a Hawk token id and secret key. This method is essentially the reverse of decode_hawk_id. Given a userid, it returns a Hawk id and corresponding secret key. It is not needed for consuming authentication tokens, but is very useful when building them for testing purposes. """ if userid is not None: data["userid"] = userid master_secret = self.master_secret tokenid = tokenlib.make_token(data, secret=master_secret) secret = tokenlib.get_derived_secret(tokenid, secret=master_secret) return tokenid, secret
def create_token(): expires = int(time.time()) + DURATION token_data = { 'uid': LEGACY_UID, 'node': NODE, 'expires': expires, 'fxa_uid': FXA_UID, 'fxa_kid': FXA_KID, 'hashed_fxa_uid': metrics_hash(FXA_UID), 'hashed_device_id': metrics_hash(DEVICE_ID), 'salt': SALT, } token = tokenlib.make_token(token_data, secret=SECRET) key = tokenlib.get_derived_secret(token, secret=SECRET) return token, key, expires, SALT
def encode_hawk_id(self, request, userid): """Encode the given userid into a Hawk id and secret key. This method is essentially the reverse of decode_hawk_id. It is not needed for consuming authentication tokens, but is very useful when building them for testing purposes. """ node_name = self._get_node_name(request) # There might be multiple secrets in use, if we're in the # process of transitioning from one to another. Always use # the last one aka the "most recent" secret. secret = self._get_token_secrets(node_name)[-1] data = {"uid": userid, "node": node_name} tokenid = tokenlib.make_token(data, secret=secret) key = tokenlib.get_derived_secret(tokenid, secret=secret) return tokenid, key
def test_purging_of_old_user_records(self): # Make some old user records. service = "test-1.0" email = "*****@*****.**" user = self.backend.allocate_user(service, email, client_state="a") self.backend.update_user(service, user, client_state="b") self.backend.update_user(service, user, client_state="c") user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 3) user = self.backend.get_user(service, email) self.assertEquals(user["client_state"], "c") self.assertEquals(len(user["old_client_states"]), 2) # The default grace-period should prevent any cleanup. self.assertTrue(purge_old_records(self.ini_file)) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 3) self.assertEqual(len(self.service_requests), 0) # With no grace period, we should cleanup two old records. self.assertTrue(purge_old_records(self.ini_file, grace_period=0)) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 1) self.assertEqual(len(self.service_requests), 2) # Check that the proper delete requests were made to the service. secrets = self.config.registry.settings["tokenserver.secrets"] node_secret = secrets.get(self.service_node)[-1] expected_kids = ["b", "a"] for i, environ in enumerate(self.service_requests): # They must be to the correct path. self.assertEquals(environ["REQUEST_METHOD"], "DELETE") self.assertTrue(re.match("/1.0/[0-9]+", environ["PATH_INFO"])) # They must have a correct request signature. token = hawkauthlib.get_id(environ) secret = tokenlib.get_derived_secret(token, secret=node_secret) self.assertTrue(hawkauthlib.check_signature(environ, secret)) userdata = tokenlib.parse_token(token, secret=node_secret) self.assertTrue("uid" in userdata) self.assertTrue("node" in userdata) self.assertEqual(userdata["fxa_uid"], "test") self.assertEqual(userdata["fxa_kid"], expected_kids[i]) # Check that the user's current state is unaffected user = self.backend.get_user(service, email) self.assertEquals(user["client_state"], "c") self.assertEquals(len(user["old_client_states"]), 0)
def encode_hawk_id(self, request, userid, extra=None): """Encode the given userid into a Hawk id and secret key. This method is essentially the reverse of decode_hawk_id. It is not needed for consuming authentication tokens, but is very useful when building them for testing purposes. Unlike its superclass method, this one allows the caller to specify a dict of additional user data to include in the auth token. """ node_name = self._get_node_name(request) secret = self._get_token_secrets(node_name)[-1] data = {"uid": userid, "node": node_name} if extra is not None: data.update(extra) tokenid = tokenlib.make_token(data, secret=secret) key = tokenlib.get_derived_secret(tokenid, secret=secret) return tokenid, key
def test_purging_of_old_user_records(self): # Make some old user records. service = "test-1.0" email = "*****@*****.**" user = self.backend.allocate_user(service, email, client_state="a") self.backend.update_user(service, user, client_state="b") self.backend.update_user(service, user, client_state="c") user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 3) user = self.backend.get_user(service, email) self.assertEquals(user["client_state"], "c") self.assertEquals(len(user["old_client_states"]), 2) # The default grace-period should prevent any cleanup. self.assertTrue(purge_old_records(self.ini_file)) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 3) self.assertEqual(len(self.service_requests), 0) # With no grace period, we should cleanup two old records. self.assertTrue(purge_old_records(self.ini_file, grace_period=0)) user_records = list(self.backend.get_user_records(service, email)) self.assertEqual(len(user_records), 1) self.assertEqual(len(self.service_requests), 2) # Check that the proper delete requests were made to the service. secrets = self.config.registry.settings["tokenserver.secrets"] node_secret = secrets.get(self.service_node)[-1] for environ in self.service_requests: # They must be to the correct path. self.assertEquals(environ["REQUEST_METHOD"], "DELETE") self.assertTrue(re.match("/1.0/[0-9]+", environ["PATH_INFO"])) # They must have a correct request signature. token = hawkauthlib.get_id(environ) secret = tokenlib.get_derived_secret(token, secret=node_secret) self.assertTrue(hawkauthlib.check_signature(environ, secret)) # Check that the user's current state is unaffected user = self.backend.get_user(service, email) self.assertEquals(user["client_state"], "c") self.assertEquals(len(user["old_client_states"]), 0)
def delete_service_data(config, service, uid, node, timeout=60): """Send a data-deletion request to the user's service node. This is a little bit of hackery to cause the user's service node to remove any data it still has stored for the user. We simulate a DELETE request from the user's own account. """ secrets = config.registry.settings['tokenserver.secrets'] pattern = config.registry['endpoints_patterns'][service] node_secrets = secrets.get(node) if not node_secrets: msg = "The node %r does not have any shared secret" % (node, ) raise ValueError(msg) user = {"uid": uid, "node": node} token = tokenlib.make_token(user, secret=node_secrets[-1]) secret = tokenlib.get_derived_secret(token, secret=node_secrets[-1]) endpoint = pattern.format(uid=uid, service=service, node=node) auth = HawkAuth(token, secret) resp = requests.delete(endpoint, auth=auth, timeout=timeout) if resp.status_code >= 400 and resp.status_code != 404: resp.raise_for_status()
def decode_hawk_id(self, request, tokenid): # pylint: disable=E0202 """Decode a Hawk token id into its userid and Hawk secret key. This method decodes the given Hawk token id to give the corresponding userid and Hawk secret key. It is a simple default implementation using the tokenlib library, and can be overridden by passing a callable info the HawkAuthenticationPolicy constructor. If the Hawk token id is invalid then ValueError will be raised. """ master_secret = self.master_secret secret = tokenlib.get_derived_secret(tokenid, secret=master_secret) data = tokenlib.parse_token(tokenid, secret=master_secret) userid = None for key in ("username", "userid", "uid", "email"): userid = data.get(key) if userid is not None: break else: msg = "Hawk id contains no userid" raise self.challenge(request, msg) return userid, secret
def return_token(request): """This service does the following process: - validates the BrowserID assertion provided on the Authorization header - allocates when necessary a node to the user for the required service - checks generation numbers and x-client-state header - returns a JSON mapping containing the following values: - **id** -- a signed authorization token, containing the user's id for hthe application and the node. - **secret** -- a secret derived from the shared secret - **uid** -- the user id for this servic - **api_endpoint** -- the root URL for the user for the service. """ # at this stage, we are sure that the assertion, application and version # number were valid, so let's build the authentication token and return it. backend = request.registry.getUtility(INodeAssignment) settings = request.registry.settings email = request.validated['authorization']['email'] try: idp_claims = request.validated['authorization']['idpClaims'] generation = idp_claims['fxa-generation'] if not isinstance(generation, (int, long)): raise _unauthorized("invalid-generation") except KeyError: generation = 0 application = request.validated['application'] version = request.validated['version'] pattern = request.validated['pattern'] service = get_service_name(application, version) client_state = request.validated['client-state'] with metrics_timer('tokenserver.backend.get_user', request): user = backend.get_user(service, email) if not user: allowed = settings.get('tokenserver.allow_new_users', True) if not allowed: raise _unauthorized('new-users-disabled') with metrics_timer('tokenserver.backend.allocate_user', request): user = backend.allocate_user(service, email, generation, client_state) # Update if this client is ahead of previously-seen clients. updates = {} if generation > user['generation']: updates['generation'] = generation if client_state != user['client_state']: # Don't revert from some-client-state to no-client-state. if not client_state: raise _invalid_client_state('empty string') # Don't revert to a previous client-state. if client_state in user['old_client_states']: raise _invalid_client_state('stale value') # If the IdP has been sending generation numbers, then # don't update client-state without a change in generation number. if user['generation'] > 0 and 'generation' not in updates: raise _invalid_client_state('new value with no generation change') updates['client_state'] = client_state if updates: with metrics_timer('tokenserver.backend.update_user', request): backend.update_user(service, user, **updates) # Error out if this client is behind some previously-seen client. # This is done after the updates because some other, even more up-to-date # client may have raced with a concurrent update. if user['generation'] > generation: raise _unauthorized("invalid-generation") secrets = settings['tokenserver.secrets'] node_secrets = secrets.get(user['node']) if not node_secrets: raise Exception("The specified node does not have any shared secret") secret = node_secrets[-1] # the last one is the most recent one # Clients can request a smaller token duration via an undocumented # query parameter, for testing purposes. token_duration = settings.get( 'tokenserver.token_duration', DEFAULT_TOKEN_DURATION ) try: requested_duration = int(request.params["duration"]) except (KeyError, ValueError): pass else: if 0 < requested_duration < token_duration: token_duration = requested_duration token_data = { 'uid': user['uid'], 'node': user['node'], 'expires': int(time.time()) + token_duration, 'fxa_uid': request.validated['fxa_uid'], 'device_id': request.validated['device_id'] } token = tokenlib.make_token(token_data, secret=secret) secret = tokenlib.get_derived_secret(token, secret=secret) endpoint = pattern.format( uid=user['uid'], service=service, node=user['node'] ) # To help measure user retention, include the timestamp at which we # first saw this user as part of the logs. request.metrics['uid.first_seen_at'] = user['first_seen_at'] return { 'id': token, 'key': secret, 'uid': user['uid'], 'hashed_fxa_uid': request.validated['fxa_uid'], 'api_endpoint': endpoint, 'duration': token_duration, 'hashalg': tokenlib.DEFAULT_HASHMOD }
def return_token(request): """This service does the following process: - validates the BrowserID or OAuth credentials provided in the Authorization header - allocates when necessary a node to the user for the required service - checks generation number, key-rotation timestamp and x-client-state header for consistency - returns a JSON mapping containing the following values: - **id** -- a signed authorization token, containing the user's id for hthe application and the node. - **secret** -- a secret derived from the shared secret - **uid** -- the user id for this service - **api_endpoint** -- the root URL for the user for the service. """ # at this stage, we are sure that the credentials, application and version # number were valid, so let's build the authentication token and return it. backend = request.registry.getUtility(INodeAssignment) settings = request.registry.settings email = request.validated['authorization']['email'] # The `generation` and `keys_changed_at` fields are both optional. try: idp_claims = request.validated['authorization']['idpClaims'] except KeyError: generation = 0 keys_changed_at = 0 else: generation = idp_claims.get('fxa-generation', 0) if not isinstance(generation, (int, long)): raise _unauthorized("invalid-generation") keys_changed_at = idp_claims.get('fxa-keysChangedAt', 0) if not isinstance(keys_changed_at, (int, long)): raise _unauthorized("invalid-credentials", description="invalid keysChangedAt") application = request.validated['application'] version = request.validated['version'] pattern = request.validated['pattern'] service = get_service_name(application, version) client_state = request.validated['client-state'] with metrics_timer('tokenserver.backend.get_user', request): user = backend.get_user(service, email) if not user: allowed = settings.get('tokenserver.allow_new_users', True) if not allowed: raise _unauthorized('new-users-disabled') with metrics_timer('tokenserver.backend.allocate_user', request): user = backend.allocate_user(service, email, generation, client_state, keys_changed_at=keys_changed_at) # We now perform an elaborate set of consistency checks on the # provided claims, which we expect to behave as follows: # # * `generation` is a monotonic timestamp, and increases every time # there is an authentication-related change on the user's account. # # * `keys_changed_at` is a monotonic timestamp, and increases every time # the user's keys change. This is a type of auth-related change, so # `keys_changed_at` <= `generation` at all times. # # * `client_state` is a key fingerprint and should never change back # to a previously-seen value. # # Callers who provide identity claims that violate any of these rules # either have stale credetials (in which case they should re-authenticate) # or are buggy (in which case we deny them access to the user's data). # # The logic here is slightly complicated by the fact that older versions # of the FxA server may not have been sending all the expected fields, and # that some clients do not report the `generation` timestamp. # Update if this client is ahead of previously-seen clients. updates = {} if generation > user['generation']: updates['generation'] = generation if keys_changed_at > user['keys_changed_at']: # If the caller reports a generation number, then a change # in keys should correspond to a change in generation number. # Unfortunately a previous version of the server that didn't # have `keys_changed_at` support may have already seen and # written the new value of `generation`. The best we can do # here is enforce that `keys_changed_at` <= `generation`. if generation > 0 and generation < keys_changed_at: raise _unauthorized('invalid-keysChangedAt') if generation == 0 and keys_changed_at > user['generation']: updates['generation'] = keys_changed_at updates['keys_changed_at'] = keys_changed_at if client_state != user['client_state']: # Don't revert from some-client-state to no-client-state. if not client_state: raise _invalid_client_state('empty string') # Don't revert to a previous client-state. if client_state in user['old_client_states']: raise _invalid_client_state('stale value') # If we have a generation number, then # don't update client-state without a change in generation number. if generation > 0 and 'generation' not in updates: raise _invalid_client_state('new value with no generation change') # If the IdP has been sending keys_changed_at timestamps, then # don't update client-state without a change in keys_changed_at. if user['keys_changed_at'] > 0 and 'keys_changed_at' not in updates: raise _invalid_client_state( 'new value with no keys_changed_at change') updates['client_state'] = client_state if updates: with metrics_timer('tokenserver.backend.update_user', request): backend.update_user(service, user, **updates) # Error out if this client provided a generation number, but it is behind # the generation number of some previously-seen client. if generation > 0 and user['generation'] > generation: raise _unauthorized("invalid-generation") # Error out if we previously saw a keys_changed_at for this user, but they # haven't provided one or it's earlier than previously seen. This means # that once the IdP starts sending keys_changed_at, we'll error out if it # stops (because we can't generate a proper `fxa_kid` in this case). if user['keys_changed_at'] > 0: if user['keys_changed_at'] > keys_changed_at: raise _unauthorized("invalid-keysChangedAt") secrets = settings['tokenserver.secrets'] node_secrets = secrets.get(user['node']) if not node_secrets: raise Exception("The specified node does not have any shared secret") secret = node_secrets[-1] # the last one is the most recent one # Clients can request a smaller token duration via an undocumented # query parameter, for testing purposes. token_duration = settings.get('tokenserver.token_duration', DEFAULT_TOKEN_DURATION) try: requested_duration = int(request.params["duration"]) except (KeyError, ValueError): pass else: if 0 < requested_duration < token_duration: token_duration = requested_duration token_data = { 'uid': user['uid'], 'node': user['node'], 'expires': int(time.time()) + token_duration, 'fxa_uid': request.validated['fxa_uid'], 'fxa_kid': format_key_id( # Follow FxA behaviour of using generation as a fallback. user['keys_changed_at'] or user['generation'], client_state.decode('hex')), 'hashed_fxa_uid': request.validated['hashed_fxa_uid'], 'hashed_device_id': request.validated['hashed_device_id'] } token = tokenlib.make_token(token_data, secret=secret) secret = tokenlib.get_derived_secret(token, secret=secret) endpoint = pattern.format(uid=user['uid'], service=service, node=user['node']) # To help measure user retention, include the timestamp at which we # first saw this user as part of the logs. request.metrics['uid.first_seen_at'] = user['first_seen_at'] # To help segmented analysis of client-side metrics, we can tell # clients to tag their metrics with a "node type" string that is # at much coarser granularity than the individual node name. try: node_type = settings['tokenserver.node_type_classifier'](user['node']) except KeyError: node_type = None request.metrics['node_type'] = node_type return { 'id': token, 'key': secret, 'uid': user['uid'], 'api_endpoint': endpoint, 'duration': token_duration, 'hashalg': tokenlib.DEFAULT_HASHMOD, # Extra stuff for clients to include in telemetry. 'hashed_fxa_uid': request.validated['hashed_fxa_uid'], 'node_type': node_type, }
# The token failed to validate using any secret. logger.warn("Authentication Failed: invalid hawk id") raise ValueError("invalid Hawk id") # Let the app access all user data from the token. request.user.update(data) request.metrics["metrics_uid"] = data.get("hashed_fxa_uid") request.metrics["metrics_device_id"] = data.get("hashed_device_id") # Sanity-check that we're on the right node. if data["node"] != node_name: msg = "incorrect node for this token: %s" raise ValueError(msg % (data["node"],)) # Calculate the matching request-signing secret. key = tokenlib.get_derived_secret(tokenid, secret=secret) return userid, key def encode_hawk_id(self, request, userid, extra=None): """Encode the given userid into a Hawk id and secret key. This method is essentially the reverse of decode_hawk_id. It is not needed for consuming authentication tokens, but is very useful when building them for testing purposes. Unlike its superclass method, this one allows the caller to specify a dict of additional user data to include in the auth token. """ node_name = self._get_node_name(request) secret = self._get_token_secrets(node_name)[-1]
class SyncStorageAuthenticationPolicy(TokenServerAuthenticationPolicy): """Pyramid authentication policy with special handling of expired tokens. This class extends the standard mozsvc TokenServerAuthenticationPolicy to (carefully) allow some access by holders of expired tokens. Presenting an expired token will result in a principal of "expired:<uid>" rather than just "<uid>", allowing this case to be specially detected and handled for some resources without interfering with the usual authentication rules. """ implements(IAuthenticationPolicy) def __init__(self, secrets=None, **kwds): self.expired_token_timeout = kwds.pop("expired_token_timeout", None) if self.expired_token_timeout is None: self.expired_token_timeout = DEFAULT_EXPIRED_TOKEN_TIMEOUT super(SyncStorageAuthenticationPolicy, self).__init__(secrets, **kwds) @classmethod def _parse_settings(cls, settings): """Parse settings for an instance of this class.""" supercls = super(SyncStorageAuthenticationPolicy, cls) kwds = supercls._parse_settings(settings) expired_token_timeout = settings.pop("expired_token_timeout", None) if expired_token_timeout is not None: kwds["expired_token_timeout"] = int(expired_token_timeout) return kwds def decode_hawk_id(self, request, tokenid): """Decode a Hawk token id into its userid and secret key. This method determines the appropriate secrets to use for the given request, then passes them on to tokenlib to handle the given Hawk token. If the id is invalid then ValueError will be raised. Unlike the superclass method, this implementation allows expired tokens to be used up to a configurable timeout. The effective userid for expired tokens is changed to be "expired:<uid>". """ now = time.time() node_name = self._get_node_name(request) # There might be multiple secrets in use, # so try each until we find one that works. secrets = self._get_token_secrets(node_name) for secret in secrets: try: tm = tokenlib.TokenManager(secret=secret) # Check for a proper valid signature first. # If that failed because of an expired token, check if # it falls within the allowable expired-token window. try: data = tm.parse_token(tokenid, now=now) except tokenlib.errors.ExpiredTokenError: recently = now - self.expired_token_timeout data = tm.parse_token(tokenid, now=recently) data["uid"] = "expired:%d" % (data["uid"], ) except ValueError: # Token validation failed, move on to the next secret. continue else: # Token validation succeeded, quit the loop. break else: # The token failed to validate using any secret. log_cef("Authentication Failed: invalid hawk id", 5, request.environ, request.registry.settings, "", signature=AUTH_FAILURE) raise ValueError("invalid Hawk id") # Sanity-check the contained data. # Any errors raise ValueError, triggering auth failure. try: userid = data["uid"] token_node_name = data["node"] except KeyError, e: msg = "missing value in token data: %s" raise ValueError(msg % (e, )) if token_node_name != node_name: msg = "incorrect node for this token: %s" raise ValueError(msg % (token_node_name, )) # Calculate the matching request-signing secret. key = tokenlib.get_derived_secret(tokenid, secret=secret) return userid, key
def return_token(request): """This service does the following process: - validates the BrowserID assertion provided on the Authorization header - allocates when necessary a node to the user for the required service - checks generation numbers and x-client-state header - returns a JSON mapping containing the following values: - **id** -- a signed authorization token, containing the user's id for hthe application and the node. - **secret** -- a secret derived from the shared secret - **uid** -- the user id for this servic - **api_endpoint** -- the root URL for the user for the service. """ # at this stage, we are sure that the assertion, application and version # number were valid, so let's build the authentication token and return it. backend = request.registry.getUtility(INodeAssignment) settings = request.registry.settings email = request.validated['assertion']['email'] try: idp_claims = request.validated['assertion']['idpClaims'] generation = idp_claims['fxa-generation'] if not isinstance(generation, (int, long)): raise _unauthorized("invalid-generation") except KeyError: generation = 0 application = request.validated['application'] version = request.validated['version'] pattern = request.validated['pattern'] service = get_service_name(application, version) client_state = request.validated['client-state'] with metrics_timer('tokenserver.backend.get_user', request): user = backend.get_user(service, email) if not user: allowed = settings.get('tokenserver.allow_new_users', True) if not allowed: raise _unauthorized('invalid-credentials') with metrics_timer('tokenserver.backend.allocate_user', request): user = backend.allocate_user(service, email, generation, client_state) # Update if this client is ahead of previously-seen clients. updates = {} if generation > user['generation']: updates['generation'] = generation if client_state != user['client_state']: # Don't revert from some-client-state to no-client-state. if not client_state: raise _unauthorized('invalid-client-state') # Don't revert to a previous client-state. if client_state in user['old_client_states']: raise _unauthorized('invalid-client-state') # If the IdP has been sending generation numbers, then # don't update client-state without a change in generation number. if user['generation'] > 0 and 'generation' not in updates: raise _unauthorized('invalid-client-state') updates['client_state'] = client_state if updates: with metrics_timer('tokenserver.backend.update_user', request): backend.update_user(service, user, **updates) # Error out if this client is behind some previously-seen client. # This is done after the updates because some other, even more up-to-date # client may have raced with a concurrent update. if user['generation'] > generation: raise _unauthorized("invalid-generation") secrets = settings['tokenserver.secrets'] node_secrets = secrets.get(user['node']) if not node_secrets: raise Exception("The specified node does not have any shared secret") secret = node_secrets[-1] # the last one is the most recent one token_duration = settings.get( 'tokenserver.token_duration', DEFAULT_TOKEN_DURATION ) try: requested_duration = int(request.params["duration"]) except (KeyError, ValueError): pass else: if 0 < requested_duration < token_duration: token_duration = requested_duration token_data = { 'uid': user['uid'], 'node': user['node'], 'expires': int(time.time()) + token_duration, } token = tokenlib.make_token(token_data, secret=secret) secret = tokenlib.get_derived_secret(token, secret=secret) endpoint = pattern.format( uid=user['uid'], service=service, node=user['node'] ) return {'id': token, 'key': secret, 'uid': user['uid'], 'api_endpoint': endpoint, 'duration': token_duration, 'hashalg': tokenlib.DEFAULT_HASHMOD}
def decode_hawk_id(self, request, tokenid): """Decode a Hawk token id into its userid and secret key. This method determines the appropriate secrets to use for the given request, then passes them on to tokenlib to handle the given Hawk token. If the id is invalid then ValueError will be raised. Unlike the superclass method, this implementation allows expired tokens to be used up to a configurable timeout. The effective userid for expired tokens is changed to be "expired:<uid>". """ now = time.time() node_name = self._get_node_name(request) # There might be multiple secrets in use, # so try each until we find one that works. secrets = self._get_token_secrets(node_name) for secret in secrets: try: tm = tokenlib.TokenManager(secret=secret) # Check for a proper valid signature first. # If that failed because of an expired token, check if # it falls within the allowable expired-token window. try: data = self._parse_token(tm, tokenid, now) userid = data["uid"] except tokenlib.errors.ExpiredTokenError: recently = now - self.expired_token_timeout data = self._parse_token(tm, tokenid, recently) # We replace the uid with a special string to ensure that # calling code doesn't accidentally treat the token as # valid. If it wants to use the expired uid, it will have # to explicitly dig it back out from `request.user`. data["expired_uid"] = data["uid"] userid = data["uid"] = "expired:%d" % (data["uid"],) except tokenlib.errors.InvalidSignatureError: # Token signature check failed, try the next secret. continue except TypeError as e: # Something went wrong when validating the contained data. raise ValueError(str(e)) else: # Token signature check succeeded, quit the loop. break else: # The token failed to validate using any secret. print("warn Authentication Failed: invalid hawk id") raise ValueError("invalid Hawk id") # Let the app access all user data from the token. request.user.update(data) request.metrics["metrics_uid"] = data.get("hashed_fxa_uid") request.metrics["metrics_device_id"] = data.get("hashed_device_id") # Sanity-check that we're on the right node. if data["node"] != node_name: msg = "incorrect node for this token: %s" raise ValueError(msg % (data["node"],)) # Calculate the matching request-signing secret. key = tokenlib.get_derived_secret(tokenid, secret=secret) return userid, key