def valid_authorization(request, **kwargs): """Validate that the Authorization on the request is correct. If not, add errors in the response so that the client can know what happened. """ authz = request.headers.get('Authorization') if authz is None: raise _unauthorized() authz = authz.split(None, 1) if len(authz) != 2: raise _unauthorized() name, token = authz if name.lower() == 'browserid': _validate_browserid_assertion(request, token) elif name.lower() == 'bearer': _validate_oauth_token(request, token) else: resp = _unauthorized(description='Unsupported') resp.www_authenticate = ('BrowserID', {}) raise resp authorization = request.validated['authorization'] email = authorization['email'] request.validated['fxa_uid'] = email.split("@", 1)[0] # For metrics purposes we expose a "anonymized" version of the # FxA uid which as been hmaced with a server-side secret key. # We call this the "metrics uid" and it correlates to identifiers # used in other systems, such as client-side telemetry. # # For legacy reasons the active_counts.lua script expects a longer # id stored in the key "uid", while other scripts can accept a # shorter id stored in the key "metrics_uid". request.metrics['email'] = email id_key = request.registry.settings.get("fxa.metrics_uid_secret_key") if id_key is None: id_key = 'insecure' hashed_fxa_uid_full = fxa_metrics_hash(email, id_key) hashed_fxa_uid = hashed_fxa_uid_full[:32] request.metrics['uid'] = hashed_fxa_uid_full request.metrics['metrics_uid'] = hashed_fxa_uid # Similarly, we expose an "anonymized" device-id for metrics purposes # where available. try: device = authorization['idpClaims']['fxa-deviceId'] if device is None: device = 'none' except KeyError: device = 'none' hashed_device_id = fxa_metrics_hash(hashed_fxa_uid + device, id_key)[:32] request.metrics['metrics_device_id'] = hashed_device_id # We also pass the metrics id back to the client so it # can include that in its own metrics events. request.validated['hashed_fxa_uid'] = hashed_fxa_uid request.validated['hashed_device_id'] = hashed_device_id
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_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 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
def valid_authorization(request, **kwargs): """Validate that the Authorization on the request is correct and valid. If authorization is valid, this validator populates user information into `request.validated` as follows: * `authorization`: a dict of information about the user: * `email`: user id in the form "{userid}@{issuer}" * `idpClaims`: a dict of optional extra claims from the IdP: * `fxa-generation`: timestamp at which user credentials last changed * `fxa-keysChangedAt`: timestamp at which user keys last changed * `fxa_uid`: the userid component of `authorization.email` * `client_state`: hash of the user's key material, as a hex string * `hashed_fxa_uid`: hmaced `fxa_uid`, to use for metrics * `hashed_device_id`: hmaced device identifier, to use for metrics If authorization is not valid, this validator adds errors in the response so that the client can know what happened. """ authz = request.headers.get('Authorization') if authz is None: raise _unauthorized() authz = authz.split(None, 1) if len(authz) != 2: raise _unauthorized() name, token = authz if name.lower() == 'browserid': _validate_browserid_assertion(request, token) elif name.lower() == 'bearer': _validate_oauth_token(request, token) else: resp = _unauthorized(description='Unsupported') resp.www_authenticate = ('BrowserID', {}) raise resp authorization = request.validated['authorization'] email = authorization['email'] request.validated['fxa_uid'] = email.split("@", 1)[0] # For metrics purposes we expose a "anonymized" version of the # FxA uid which as been hmaced with a server-side secret key. # We call this the "metrics uid" and it correlates to identifiers # used in other systems, such as client-side telemetry. # # For legacy reasons the active_counts.lua script expects a longer # id stored in the key "uid", while other scripts can accept a # shorter id stored in the key "metrics_uid". request.metrics['email'] = email id_key = request.registry.settings.get("fxa.metrics_uid_secret_key") if id_key is None: id_key = 'insecure' hashed_fxa_uid_full = fxa_metrics_hash(email, id_key) hashed_fxa_uid = hashed_fxa_uid_full[:32] request.metrics['uid'] = hashed_fxa_uid_full request.metrics['metrics_uid'] = hashed_fxa_uid # Similarly, we expose an "anonymized" device-id for metrics purposes # where available. try: device = authorization['idpClaims']['fxa-deviceId'] if device is None: device = 'none' except KeyError: device = 'none' hashed_device_id = fxa_metrics_hash(hashed_fxa_uid + device, id_key)[:32] request.metrics['metrics_device_id'] = hashed_device_id # We also pass the metrics id back to the client so it # can include that in its own metrics events. request.validated['hashed_fxa_uid'] = hashed_fxa_uid request.validated['hashed_device_id'] = hashed_device_id