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