Exemplo n.º 1
0
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
Exemplo n.º 2
0
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")
Exemplo n.º 3
0
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")
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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

    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")

    try:
        verifier = get_verifier()
        with metrics_timer('tokenserver.assertion.verify', request):
            assertion = verifier.verify(assertion)
    except BrowserIDError as e:
        _handle_exception(e.__class__.__name__)

    # 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
Exemplo n.º 6
0
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

    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")

    try:
        verifier = get_verifier()
        with metrics_timer('tokenserver.assertion.verify', request):
            assertion = verifier.verify(assertion)
    except BrowserIDError as e:
        _handle_exception(e.__class__.__name__)

    # 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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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}
Exemplo n.º 10
0
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,
    }
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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
    }
Exemplo n.º 13
0
 def test_timing_contextmanager_doesnt_fail_if_no_reqest_object(self):
     with metrics_timer("timer1"):
         time.sleep(0.01)
Exemplo n.º 14
0
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
Exemplo n.º 15
0
 def test_timing_contextmanager_doesnt_fail_if_no_reqest_object(self):
     with metrics_timer("timer1"):
         time.sleep(0.01)
Exemplo n.º 16
0
 def viewit(request):
     with metrics_timer("timer1"):
         time.sleep(0.01)
Exemplo n.º 17
0
 def viewit(request):
     with metrics_timer("timer1"):
         time.sleep(0.01)