Exemple #1
0
def check_bearer_delegation_token(token, peer_identity, auth_db=None):
    """Decodes the token, checks its validity, extracts delegated Identity.

  Logs details about the token.

  Args:
    token: blob with base64 encoded delegation token.
    peer_identity: Identity of whoever tries to wield the token.
    auth_db: AuthDB instance with groups, defaults to get_request_auth_db().

  Returns:
    (Delegated Identity, validated delegation_pb2.Subtoken proto).

  Raises:
    BadTokenError if token is invalid.
    TransientError if token can't be verified due to transient errors.
  """
    logging.info('Checking delegation token: fingerprint=%s',
                 utils.get_token_fingerprint(token))
    subtoken = unseal_token(deserialize_token(token))
    if subtoken.kind != delegation_pb2.Subtoken.BEARER_DELEGATION_TOKEN:
        raise exceptions.BadTokenError(
            'Not a valid delegation token kind: %s' % subtoken.kind)
    ident = check_subtoken(subtoken, peer_identity, auth_db
                           or api.get_request_auth_db())
    logging.info(
        'Using delegation token: subtoken_id=%s, delegated_identity=%s',
        subtoken.subtoken_id, ident.to_bytes())
    return ident, subtoken
def _check_and_log_token(flavor, account_email, token):
    """Checks the lifetime and logs details about the generated access token."""
    expires_in = token.expiry - utils.time_time()
    logging.info(
        'Got %s access token: email=%s, fingerprint=%s, expiry=%d, expiry_in=%d',
        flavor, account_email, utils.get_token_fingerprint(token.access_token),
        token.expiry, expires_in)
    # Give 2 min of wiggle room to account for various effects related to
    # relativity of clocks (us vs Google backends that produce the token) and
    # indeterminism of network propagation delays. 2 min should be more than
    # enough to account for them. These asserts should never be hit.
    assert expires_in < MAX_TOKEN_LIFETIME_SEC + 60
    assert expires_in > MIN_TOKEN_LIFETIME_SEC - 60
Exemple #3
0
def _log_jwt(email, method, jwt):
  """Logs information about the signed JWT.

  Does some minimal validation which fails only if Google backends misbehave,
  which should not happen. Logs broken JWTs, assuming they are unusable.
  """
  parts = jwt.split('.')
  if len(parts) != 3:
    logging.error(
        'Got broken JWT (not <hdr>.<claims>.<sig>): by=%s method=%s jwt=%r',
        email, method, jwt)
    raise AccessTokenError('Got broken JWT, see logs')

  try:
    hdr = _b64_decode(parts[0])     # includes key ID
    claims = _b64_decode(parts[1])  # includes scopes and timestamp
    sig = parts[2][:12]             # only 9 bytes of the signature
  except (TypeError, ValueError):
    logging.error(
        'Got broken JWT (can\'t base64-decode): by=%s method=%s jwt=%r',
        email, method, jwt)
    raise AccessTokenError('Got broken JWT, see logs')

  if not _is_json_object(hdr):
    logging.error(
        'Got broken JWT (the header is not JSON dict): by=%s method=%s jwt=%r',
        email, method, jwt)
    raise AccessTokenError('Got broken JWT, see logs')
  if not _is_json_object(claims):
    logging.error(
        'Got broken JWT (claims are not JSON dict): by=%s method=%s jwt=%r',
        email, method, jwt)
    raise AccessTokenError('Got broken JWT, see logs')

  logging.info(
      'signed_jwt: by=%s method=%s hdr=%s claims=%s sig_prefix=%s fp=%s',
      email, method, hdr, claims, sig, utils.get_token_fingerprint(jwt))
Exemple #4
0
def delegate_async(audience,
                   services,
                   min_validity_duration_sec=5 * 60,
                   max_validity_duration_sec=60 * 60 * 3,
                   impersonate=None,
                   tags=None,
                   token_server_url=None):
    """Creates a delegation token by contacting the token server.

  Memcaches the token.

  Args:
    audience (list of (str or Identity)): to WHOM caller's identity is
      delegated; a list of identities or groups, a string "REQUESTOR" (to
      indicate the current service) or symbol '*' (which means ANY).
      Example: ['user:[email protected]', 'group:abcdef', 'REQUESTOR'].
    services (list of (str or Identity)): WHERE token is accepted.
      Each list element must be an identity of 'service' kind, a root URL of a
      service (e.g. 'https://....'), or symbol '*'.
      Example: ['service:gae-app1', 'https://gae-app2.appspot.com']
    min_validity_duration_sec (int): minimally acceptable lifetime of the token.
      If there's existing token cached locally that have TTL
      min_validity_duration_sec or more, it will be returned right away.
      Default is 5 min.
    max_validity_duration_sec (int): defines lifetime of a new token.
      It will bet set as tokens' TTL if there's no existing cached tokens with
      sufficiently long lifetime. Default is 3 hours.
    impersonate (str or Identity): a caller can mint a delegation token on
      someone else's behalf (effectively impersonating them). Only a privileged
      set of callers can do that. If impersonation is allowed, token's
      delegated_identity field will contain whatever is in 'impersonate' field.
      Example: 'user:[email protected]'
    tags (list of str): optional list of key:value pairs to embed into the
      token. Services that accept the token may use them for additional
      authorization decisions.
    token_server_url (str): the URL for the token service that will mint the
      token. Defaults to the URL provided by the primary auth service.

  Returns:
    DelegationToken as ndb.Future.

  Raises:
    ValueError if args are invalid.
    TokenCreationError if could not create a token.
    TokenAuthorizationError on HTTP 403 response from auth service.
  """
    assert isinstance(audience, list), audience
    assert isinstance(services, list), services

    id_to_str = lambda i: i.to_bytes() if isinstance(i, model.Identity) else i

    # Validate audience.
    if '*' in audience:
        audience = ['*']
    else:
        if not audience:
            raise ValueError('audience can\'t be empty')
        for a in audience:
            if isinstance(a, model.Identity):
                continue  # identities are already validated
            if not isinstance(a, basestring):
                raise ValueError('expecting a string or Identity')
            if a == 'REQUESTOR' or a.startswith('group:'):
                continue
            # The only remaining option is a string that represents an identity.
            # Validate it. from_bytes may raise ValueError.
            model.Identity.from_bytes(a)
        audience = sorted(map(id_to_str, audience))

    # Validate services.
    if '*' in services:
        services = ['*']
    else:
        if not services:
            raise ValueError('services can\'t be empty')
        for s in services:
            if isinstance(s, basestring):
                if s.startswith('https://'):
                    continue  # an URL, the token server knows how to handle it
                s = model.Identity.from_bytes(s)
            assert isinstance(s, model.Identity), s
            assert s.kind == model.IDENTITY_SERVICE, s
        services = sorted(map(id_to_str, services))

    # Validate validity durations.
    assert isinstance(min_validity_duration_sec,
                      int), min_validity_duration_sec
    assert isinstance(max_validity_duration_sec,
                      int), max_validity_duration_sec
    assert min_validity_duration_sec >= 5
    assert max_validity_duration_sec >= 5
    assert min_validity_duration_sec <= max_validity_duration_sec

    # Validate impersonate.
    if impersonate is not None:
        assert isinstance(impersonate,
                          (basestring, model.Identity)), impersonate
        impersonate = id_to_str(impersonate)

    # Validate tags.
    tags = sorted(tags or [])
    for tag in tags:
        parts = tag.split(':', 1)
        if len(parts) != 2 or parts[0] == '' or parts[1] == '':
            raise ValueError('Bad delegation token tag: %r' % tag)

    # Grab the token service URL.
    if not token_server_url:
        token_server_url = api.get_request_auth_db().token_server_url
        if not token_server_url:
            raise exceptions.TokenCreationError(
                'Token server URL is not configured')

    # End of validation.

    # See MintDelegationTokenRequest in
    # https://github.com/luci/luci-go/blob/master/tokenserver/api/minter/v1/token_minter.proto.
    req = {
        'delegatedIdentity': impersonate or 'REQUESTOR',
        'validityDuration': max_validity_duration_sec,
        'audience': audience,
        'services': services,
        'tags': tags,
    }

    # Get from cache.
    cache_key_hash = hashlib.sha256(
        token_server_url + '\n' + json.dumps(req, sort_keys=True)).hexdigest()
    cache_key = 'delegation_token/v2/%s' % cache_key_hash
    ctx = ndb.get_context()
    token = yield ctx.memcache_get(cache_key)
    min_validity_duration = datetime.timedelta(
        seconds=min_validity_duration_sec)
    now = utils.utcnow()
    if token and token.expiry - min_validity_duration > now:
        logging.info('Fetched cached delegation token: fingerprint=%s',
                     utils.get_token_fingerprint(token.token))
        raise ndb.Return(token)

    # Request a new one.
    logging.info(
        'Minting a delegation token for %r',
        {k: v
         for k, v in req.iteritems() if v},
    )
    res = yield service_account.authenticated_request_async(
        '%s/prpc/tokenserver.minter.TokenMinter/MintDelegationToken' %
        token_server_url,
        method='POST',
        payload=req)

    signed_token = res.get('token')
    if not signed_token or not isinstance(signed_token, basestring):
        logging.error('Bad MintDelegationToken response: %s', res)
        raise exceptions.TokenCreationError('Bad response, no token')

    token_struct = res.get('delegationSubtoken')
    if not token_struct or not isinstance(token_struct, dict):
        logging.error('Bad MintDelegationToken response: %s', res)
        raise exceptions.TokenCreationError(
            'Bad response, no delegationSubtoken')

    if token_struct.get('kind') != 'BEARER_DELEGATION_TOKEN':
        logging.error('Bad MintDelegationToken response: %s', res)
        raise exceptions.TokenCreationError(
            'Bad response, not BEARER_DELEGATION_TOKEN')

    actual_validity_duration_sec = token_struct.get('validityDuration')
    if not isinstance(actual_validity_duration_sec, (int, float)):
        logging.error('Bad MintDelegationToken response: %s', res)
        raise exceptions.TokenCreationError(
            'Unexpected response, validityDuration is absent or not a number')

    token = DelegationToken(
        token=str(signed_token),
        expiry=now + datetime.timedelta(seconds=actual_validity_duration_sec),
    )

    logging.info(
        'Token server "%s" generated token (subtoken_id=%s, fingerprint=%s):\n%s',
        res.get('serviceVersion'), token_struct.get('subtokenId'),
        utils.get_token_fingerprint(token.token),
        json.dumps(res.get('delegationSubtoken'),
                   sort_keys=True,
                   indent=2,
                   separators=(',', ': ')))

    # Put to cache. Refresh the token 10 sec in advance.
    if actual_validity_duration_sec > 10:
        yield ctx.memcache_add(cache_key,
                               token,
                               time=actual_validity_duration_sec - 10)

    raise ndb.Return(token)
 def test_get_token_fingerprint(self):
   self.assertEqual(
       '8b7df143d91c716ecfa5fc1730022f6b',
       utils.get_token_fingerprint(u'blah'))
def _log_token_grant(prefix, token, exp_ts, log_call=logging.info):
    """Logs details about an OAuth token grant."""
    ts = utils.datetime_to_timestamp(exp_ts) / 1e6
    log_call('%s OAuth token grant: fingerprint=%s, expiry=%d, expiry_in=%d',
             prefix, utils.get_token_fingerprint(token), ts,
             ts - utils.time_time())