Example #1
0
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
Example #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")
Example #3
0
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
Example #4
0
    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()
Example #5
0
    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")
Example #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

    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)
Example #7
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.
    """
    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
Example #8
0
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
Example #9
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
Example #10
0
def _unauthorized(status_message='error', **kw):
    kw.setdefault('description', 'Unauthorized')
    return json_error(401, status_message, **kw)
Example #11
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
Example #12
0
 def _unauthorized():
     return json_error(401, description='Unauthorized')
Example #13
0
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}