def valid_app(request, **kwargs): """Checks that the given application is one of the compatible ones. If it's not the case, a 404 is issued with the appropriate information. """ supported = request.registry.settings['tokenserver.applications'] application = request.matchdict.get('application') version = request.matchdict.get('version') if application not in supported: raise json_error(404, location='url', name='application', description='Unsupported application') else: request.validated['application'] = application supported_versions = supported[application] if version not in supported_versions: raise json_error(404, location='url', name=version, description='Unsupported application version') else: request.validated['version'] = version
def valid_app(request): """Checks that the given application is one of the compatible ones. If it's not the case, a 404 is issued with the appropriate information. """ supported = request.registry.settings['tokenserver.applications'] application = request.matchdict.get('application') version = request.matchdict.get('version') if application not in supported: raise json_error(404, location='url', name='application', description='Unsupported application') else: request.validated['application'] = application supported_versions = supported[application] if version not in supported_versions: raise json_error(404, location='url', name=version, description='Unsupported application version') else: request.validated['version'] = version accepted = (request.headers.get('X-Conditions-Accepted', None) is not None) request.validated['x-conditions-accepted'] = accepted
def _validate_oauth_token(request, token): try: verifier = get_oauth_verifier(request.registry) except ComponentLookupError: raise _unauthorized(description='Unsupported') try: with metrics_timer('tokenserver.oauth.verify', request): token = verifier.verify(token) except (fxa.errors.Error, ConnectionError) as e: request.metrics['token.oauth.verify_failure'] = 1 if isinstance(e, fxa.errors.InProtocolError): request.metrics['token.oauth.errno.%s' % e.errno] = 1 # Log a full traceback for errors that are not a simple # "your token was bad and we dont trust it". if not isinstance(e, fxa.errors.TrustError): if not isinstance(e, fxa.errors.InProtocolError): logger.exception("Unexpected verification error") elif e.errno not in OAUTH_EXPECTED_ERRNOS: logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, ConnectionError): request.metrics['token.oauth.connection_error'] = 1 raise json_error(503, description="Resource is not available") raise _unauthorized("invalid-credentials") request.metrics['token.oauth.verify_success'] = 1 request.validated['authorization'] = token # OAuth clients should send the scoped-key kid in lieu of X-Client-State. # A future enhancement might allow us to learn this from the OAuth # verification response rather than requiring a separate header. kid = request.headers.get('X-KeyID') if kid: try: # The kid combines a timestamp and a hash of the key material, # so we can decode it into equivalent information to what we # get out of a BrowserID assertion. generation, client_state = kid.split("-", 1) generation = int(generation) idpClaims = request.validated['authorization']['idpClaims'] idpClaims['fxa-generation'] = generation client_state = browserid.utils.decode_bytes(client_state) client_state = client_state.encode('hex') if not 1 <= len(client_state) <= 32: raise json_error(400, location='header', name='X-Client-State', description='Invalid client state value') # Sanity-check in case the client sent *both* headers. # If they don't match, the client is definitely confused. if 'X-Client-State' in request.headers: if request.headers['X-Client-State'] != client_state: raise _unauthorized("invalid-client-state") request.validated['client-state'] = client_state except (IndexError, ValueError): raise _unauthorized("invalid-credentials")
def _validate_oauth_token(request, token): try: verifier = get_oauth_verifier(request.registry) except ComponentLookupError: raise _unauthorized(description='Unsupported') try: with metrics_timer('tokenserver.oauth.verify', request): token = verifier.verify(token) except (fxa.errors.Error, ConnectionError) as e: request.metrics['token.oauth.verify_failure'] = 1 if isinstance(e, fxa.errors.InProtocolError): request.metrics['token.oauth.errno.%s' % e.errno] = 1 # Log a full traceback for errors that are not a simple # "your token was bad and we dont trust it". if not isinstance(e, fxa.errors.TrustError): logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, ConnectionError): request.metrics['token.oauth.connection_error'] = 1 raise json_error(503, description="Resource is not available") raise _unauthorized("invalid-credentials") request.metrics['token.oauth.verify_success'] = 1 request.validated['authorization'] = token # OAuth clients should send the scoped-key kid in lieu of X-Client-State. # A future enhancement might allow us to learn this from the OAuth # verification response rather than requiring a separate header. kid = request.headers.get('X-KeyID') if kid: try: # The kid combines a timestamp and a hash of the key material, # so we can decode it into equivalent information to what we # get out of a BrowserID assertion. generation, client_state = kid.split("-", 1) generation = int(generation) idpClaims = request.validated['authorization']['idpClaims'] idpClaims['fxa-generation'] = generation client_state = browserid.utils.decode_bytes(client_state) client_state = client_state.encode('hex') if not 1 <= len(client_state) <= 32: raise json_error(400, location='header', name='X-Client-State', description='Invalid client state value') # Sanity-check in case the client sent *both* headers. # If they don't match, the client is definitely confused. if 'X-Client-State' in request.headers: if request.headers['X-Client-State'] != client_state: raise _unauthorized("invalid-client-state") request.validated['client-state'] = client_state except (IndexError, ValueError): raise _unauthorized("invalid-credentials")
def _valid_browserid_assertion(request, assertion): try: verifier = get_browserid_verifier(request.registry) with metrics_timer('tokenserver.assertion.verify', request): assertion = verifier.verify(assertion) except browserid.errors.Error as e: # Convert CamelCase to under_scores for reporting. error_type = e.__class__.__name__ error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() request.metrics['token.assertion.verify_failure'] = 1 request.metrics['token.assertion.%s' % error_type] = 1 # Log a full traceback for errors that are not a simple # "your assertion was bad and we dont trust it". if not isinstance(e, browserid.errors.TrustError): logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, browserid.errors.ConnectionError): raise json_error(503, description="Resource is not available") if isinstance(e, browserid.errors.ExpiredSignatureError): raise _unauthorized("invalid-timestamp") raise _unauthorized("invalid-credentials") # FxA sign-in confirmation introduced the notion of unverified tokens. # The default value is True to preserve backwards compatibility. try: tokenVerified = assertion['idpClaims']['fxa-tokenVerified'] except KeyError: tokenVerified = True if not tokenVerified: raise _unauthorized("invalid-credentials") # everything sounds good, add the assertion to the list of validated fields # and continue request.metrics['token.assertion.verify_success'] = 1 request.validated['authorization'] = assertion id_key = request.registry.settings.get("fxa.metrics_uid_secret_key") if id_key is None: id_key = 'insecure' email = assertion['email'] fxa_uid_full = fxa_metrics_hash(email, id_key) # "legacy" key used by heka active_counts.lua request.metrics['uid'] = fxa_uid_full request.metrics['email'] = email # "new" keys use shorter values fxa_uid = fxa_uid_full[:32] request.validated['fxa_uid'] = fxa_uid request.metrics['fxa_uid'] = fxa_uid try: device = assertion['idpClaims']['fxa-deviceId'] if device is None: device = 'none' except KeyError: device = 'none' device_id = fxa_metrics_hash(fxa_uid + device, id_key)[:32] request.validated['device_id'] = device_id request.metrics['device_id'] = device_id
def valid_client_state(request, **kwargs): """Checks for and validates the X-Client-State header.""" client_state = request.headers.get('X-Client-State', '') if client_state: if not re.match("^[a-zA-Z0-9._-]{1,32}$", client_state): raise json_error(400, location='header', name='X-Client-State', description='Invalid client state value') request.validated['client-state'] = client_state
def valid_client_state(request): """Checks for and validates the X-Client-State header.""" client_state = request.headers.get('X-Client-State', '') if client_state: if not re.match("[a-zA-Z0-9._-]{1,32}", client_state): raise json_error(400, location='header', name='X-Client-State', description='Invalid client state value') request.validated['client-state'] = client_state
def _handle_exception(error_type): # convert CamelCase to camel_case error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() metlog.incr('token.assertion.verify_failure') metlog.incr('token.assertion.%s' % error_type) if error_type == "connection_error": raise json_error(503, description="Resource is not available") else: raise _unauthorized()
def _handle_exception(error_type): # convert CamelCase to camel_case error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() request.metrics['token.assertion.verify_failure'] = 1 request.metrics['token.assertion.%s' % error_type] = 1 if error_type == "connection_error": raise json_error(503, description="Resource is not available") if error_type == "expired_signature_error": raise _unauthorized("invalid-timestamp") else: raise _unauthorized("invalid-credentials")
def valid_assertion(request): """Validate that the assertion given in the request is correct. If not, add errors in the response so that the client can know what happened. """ token = request.headers.get('Authorization') if token is None: raise _unauthorized() token = token.split() if len(token) != 2: raise _unauthorized() name, assertion = token if name.lower() != 'browserid': resp = _unauthorized(description='Unsupported') resp.www_authenticate = ('BrowserID', {}) raise resp try: verifier = get_verifier() with metrics_timer('tokenserver.assertion.verify', request): assertion = verifier.verify(assertion) except browserid.errors.Error as e: # Convert CamelCase to under_scores for reporting. error_type = e.__class__.__name__ error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() request.metrics['token.assertion.verify_failure'] = 1 request.metrics['token.assertion.%s' % error_type] = 1 # Log a full traceback for errors that are not a simple # "your assertion was bad and we dont trust it". if not isinstance(e, browserid.errors.TrustError): logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, browserid.errors.ConnectionError): raise json_error(503, description="Resource is not available") if isinstance(e, browserid.errors.ExpiredSignatureError): raise _unauthorized("invalid-timestamp") raise _unauthorized("invalid-credentials") # everything sounds good, add the assertion to the list of validated fields # and continue request.metrics['token.assertion.verify_success'] = 1 request.validated['assertion'] = assertion # Include a unique FxA identifier in the logs, but obfuscate # it for privacy purposes. id_key = request.registry.settings.get("fxa.metrics_uid_secret_key") if id_key: email = request.validated['assertion']['email'] request.metrics['uid'] = fxa_metrics_uid(email, id_key)
def valid_assertion(request): """Validate that the assertion given in the request is correct. If not, add errors in the response so that the client can know what happened. """ metlog = request.registry['metlog'] def _unauthorized(): return json_error(401, description='Unauthorized') token = request.headers.get('Authorization') if token is None: raise _unauthorized() token = token.split() if len(token) != 2: raise _unauthorized() name, assertion = token if name.lower() != 'browser-id': resp = json_error(401, description='Unsupported') resp.www_authenticate = ('Browser-ID', {}) raise resp def _handle_exception(error_type): # convert CamelCase to camel_case error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() metlog.incr('token.assertion.verify_failure') metlog.incr('token.assertion.%s' % error_type) if error_type == "connection_error": raise json_error(503, description="Resource is not available") else: raise _unauthorized() try: verifier = get_verifier() assertion = verifier.verify(assertion) except ClientCatchedError as e: _handle_exception(e.error_type) except BrowserIDError as e: _handle_exception(e.__class__.__name__) # everything sounds good, add the assertion to the list of validated fields # and continue metlog.incr('token.assertion.verify_success') request.validated['assertion'] = assertion
def pattern_exists(request): """Checks that the given service do have an associated pattern in the db or in the configuration file. If not, raises a 503 error. """ application = request.validated['application'] version = request.validated['version'] defined_patterns = request.registry['endpoints_patterns'] service = get_service_name(application, version) try: pattern = defined_patterns[service] except KeyError: description = "The api_endpoint pattern for %r is not known" % service raise json_error(503, description=description) request.validated['pattern'] = pattern
def pattern_exists(request, **kwargs): """Checks that the given service do have an associated pattern in the db or in the configuration file. If not, raises a 503 error. """ application = request.validated['application'] version = request.validated['version'] defined_patterns = request.registry['endpoints_patterns'] service = get_service_name(application, version) try: pattern = defined_patterns[service] except KeyError: description = "The api_endpoint pattern for %r is not known" % service raise json_error(503, description=description) request.validated['pattern'] = pattern
def _validate_browserid_assertion(request, assertion): try: verifier = get_browserid_verifier(request.registry) except ComponentLookupError: raise _unauthorized(description='Unsupported') try: with metrics_timer('tokenserver.assertion.verify', request): assertion = verifier.verify(assertion) except browserid.errors.Error as e: # Convert CamelCase to under_scores for reporting. error_type = e.__class__.__name__ error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() request.metrics['token.assertion.verify_failure'] = 1 request.metrics['token.assertion.%s' % error_type] = 1 # Log a full traceback for errors that are not a simple # "your assertion was bad and we dont trust it". if not isinstance(e, browserid.errors.TrustError): logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, browserid.errors.ConnectionError): raise json_error(503, description="Resource is not available") if isinstance(e, browserid.errors.ExpiredSignatureError): raise _unauthorized("invalid-timestamp") raise _unauthorized("invalid-credentials") # FxA sign-in confirmation introduced the notion of unverified tokens. # The default value is True to preserve backwards compatibility. try: tokenVerified = assertion['idpClaims']['fxa-tokenVerified'] except KeyError: tokenVerified = True if not tokenVerified: raise _unauthorized("invalid-credentials") # everything sounds good, add the assertion to the list of validated fields # and continue request.metrics['token.assertion.verify_success'] = 1 request.validated['authorization'] = assertion
def _unauthorized(status_message='error', **kw): kw.setdefault('description', 'Unauthorized') return json_error(401, status_message, **kw)
def return_token(request): """This service does the following process: - validates the Browser-ID assertion provided on the Authorization header - allocates when necessary a node to the user for the required service - deals with the X-Conditions-Accepted 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) email = request.validated['assertion']['email'] application = request.validated['application'] version = request.validated['version'] pattern = request.validated['pattern'] service = get_service_name(application, version) accepted = request.validated['x-conditions-accepted'] # get the node or allocate one if none is already set uid, node, to_accept = backend.get_node(email, service) if to_accept is not None: # the backend sent a tos url, meaning the user needs to # sign it, we want to compare both tos and raise a 403 # if they are not equal if not accepted: to_accept = dict([(name, value) for name, value, __ in to_accept]) raise json_error(403, location='header', description='Need to accept conditions', name='X-Conditions-Accepted', condition_urls=to_accept) # at this point, either the tos were signed or the service does not # have any ToS if node is None or uid is None: metlog = request.registry['metlog'] start = time.time() try: uid, node = backend.allocate_node(email, service) finally: duration = time.time() - start metlog.timer_send("token.sql.allocate_node", duration) secrets = request.registry.settings['tokenserver.secrets_file'] node_secrets = secrets.get(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 = request.registry.settings.get( 'tokenserver.token_duration', DEFAULT_TOKEN_DURATION) token = make_token({'uid': uid, 'service_entry': node}, timeout=token_duration, secret=secret) # XXX needs to be renamed as 'get_derived_secret' because # it's not clear here it's a derived secret = get_token_secret(token, secret=secret) api_endpoint = pattern.format(uid=uid, service=service, node=node) # FIXME add the algo used to generate the token return {'id': token, 'key': secret, 'uid': uid, 'api_endpoint': api_endpoint, 'duration': token_duration}
def _valid_oauth_token(request, token): try: verifier = get_oauth_verifier(request.registry) with metrics_timer('tokenserver.oauth.verify', request): token = verifier.verify(token) except (fxa.errors.Error, ConnectionError) as e: request.metrics['token.oauth.verify_failure'] = 1 if isinstance(e, fxa.errors.InProtocolError): request.metrics['token.oauth.errno.%s' % e.errno] = 1 # Log a full traceback for errors that are not a simple # "your token was bad and we dont trust it". if not isinstance(e, fxa.errors.TrustError): logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, ConnectionError): request.metrics['token.oauth.connection_error'] = 1 raise json_error(503, description="Resource is not available") raise _unauthorized("invalid-credentials") request.metrics['token.oauth.verify_success'] = 1 request.validated['authorization'] = token # OAuth clients should send the scoped-key kid in lieu of X-Client-State. # A future enhancement might allow us to learn this from the OAuth # verification response rather than requiring a separate header. kid = request.headers.get('X-KeyID') if kid: try: # The kid combines a timestamp and a hash of the key material, # so we can decode it into equivalent information to what we # get out of a BrowserID assertion. generation, client_state = kid.split("-", 1) generation = int(generation) idpClaims = request.validated['authorization']['idpClaims'] idpClaims['fxa-generation'] = generation client_state = browserid.utils.decode_bytes(client_state) client_state = client_state.encode('hex') if not 1 <= len(client_state) <= 32: raise json_error(400, location='header', name='X-Client-State', description='Invalid client state value') # Sanity-check in case the client sent *both* headers. # If they don't match, the client is definitely confused. if 'X-Client-State' in request.headers: if request.headers['X-Client-State'] != client_state: raise _unauthorized("invalid-client-state") request.validated['client-state'] = client_state except (IndexError, ValueError): raise _unauthorized("invalid-credentials") id_key = request.registry.settings.get("fxa.metrics_uid_secret_key") if id_key is None: id_key = 'insecure' email = token['email'] fxa_uid_full = fxa_metrics_hash(email, id_key) # "legacy" key used by heka active_counts.lua request.metrics['uid'] = fxa_uid_full request.metrics['email'] = email # "new" keys use shorter values fxa_uid = fxa_uid_full[:32] request.validated['fxa_uid'] = fxa_uid request.metrics['fxa_uid'] = fxa_uid # There's currently no notion of a "device id" in OAuth. # In future we might be able to use e.g. the refresh token # or some derivative of it here. device = 'none' device_id = fxa_metrics_hash(fxa_uid + device, id_key)[:32] request.validated['device_id'] = device_id request.metrics['device_id'] = device_id
def _unauthorized(): return json_error(401, description='Unauthorized')
def valid_assertion(request): """Validate that the assertion given in the request is correct. If not, add errors in the response so that the client can know what happened. """ token = request.headers.get('Authorization') if token is None: raise _unauthorized() token = token.split() if len(token) != 2: raise _unauthorized() name, assertion = token if name.lower() != 'browserid': resp = _unauthorized(description='Unsupported') resp.www_authenticate = ('BrowserID', {}) raise resp try: verifier = get_verifier() with metrics_timer('tokenserver.assertion.verify', request): assertion = verifier.verify(assertion) except browserid.errors.Error as e: # Convert CamelCase to under_scores for reporting. error_type = e.__class__.__name__ error_type = re.sub('(?<=.)([A-Z])', r'_\1', error_type).lower() request.metrics['token.assertion.verify_failure'] = 1 request.metrics['token.assertion.%s' % error_type] = 1 # Log a full traceback for errors that are not a simple # "your assertion was bad and we dont trust it". if not isinstance(e, browserid.errors.TrustError): logger.exception("Unexpected verification error") # Report an appropriate error code. if isinstance(e, browserid.errors.ConnectionError): raise json_error(503, description="Resource is not available") if isinstance(e, browserid.errors.ExpiredSignatureError): raise _unauthorized("invalid-timestamp") raise _unauthorized("invalid-credentials") # everything sounds good, add the assertion to the list of validated fields # and continue request.metrics['token.assertion.verify_success'] = 1 request.validated['assertion'] = assertion id_key = request.registry.settings.get("fxa.metrics_uid_secret_key") if id_key is None: id_key = 'insecure' email = assertion['email'] fxa_uid_full = fxa_metrics_hash(email, id_key) # "legacy" key used by heka active_counts.lua request.metrics['uid'] = fxa_uid_full request.metrics['email'] = email # "new" keys use shorter values fxa_uid = fxa_uid_full[:32] request.validated['fxa_uid'] = fxa_uid request.metrics['fxa_uid'] = fxa_uid try: device = assertion['idpClaims']['fxa-deviceId'] if device is None: device = 'none' except KeyError: device = 'none' device_id = fxa_metrics_hash(fxa_uid + device, id_key)[:32] request.validated['device_id'] = device_id request.metrics['device_id'] = device_id