2016-05-04 21:40:09 +00:00
|
|
|
import time
|
|
|
|
import jwt
|
2016-05-31 20:48:19 +00:00
|
|
|
import logging
|
2016-05-04 21:40:09 +00:00
|
|
|
|
2016-05-31 20:48:19 +00:00
|
|
|
from util.security import jwtutil
|
2016-05-04 21:40:09 +00:00
|
|
|
|
2016-05-31 20:48:19 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2016-05-04 21:40:09 +00:00
|
|
|
|
|
|
|
ANONYMOUS_SUB = '(anonymous)'
|
2016-05-31 20:48:19 +00:00
|
|
|
ALGORITHM = 'RS256'
|
2017-03-22 11:38:52 +00:00
|
|
|
CLAIM_TUF_ROOT = 'com.apostille.root'
|
2017-03-22 20:14:56 +00:00
|
|
|
QUAY_TUF_ROOT = 'quay'
|
|
|
|
SIGNER_TUF_ROOT = 'signer'
|
2017-04-15 12:26:33 +00:00
|
|
|
DISABLED_TUF_ROOT = '$disabled'
|
2016-05-04 21:40:09 +00:00
|
|
|
|
2016-06-27 18:17:15 +00:00
|
|
|
# The number of allowed seconds of clock skew for a JWT. The iat, nbf and exp are adjusted with this
|
2016-06-24 19:08:26 +00:00
|
|
|
# count.
|
2016-06-27 18:17:15 +00:00
|
|
|
JWT_CLOCK_SKEW_SECONDS = 30
|
2016-06-24 19:08:26 +00:00
|
|
|
|
2016-05-04 21:40:09 +00:00
|
|
|
|
2016-05-31 20:48:19 +00:00
|
|
|
class InvalidBearerTokenException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2016-09-19 20:19:29 +00:00
|
|
|
def decode_bearer_header(bearer_header, instance_keys, config):
|
2016-08-24 16:55:33 +00:00
|
|
|
""" decode_bearer_header decodes the given bearer header that contains an encoded JWT with both
|
|
|
|
a Key ID as well as the signed JWT and returns the decoded and validated JWT. On any error,
|
|
|
|
raises an InvalidBearerTokenException with the reason for failure.
|
2016-05-31 20:48:19 +00:00
|
|
|
"""
|
|
|
|
# Extract the jwt token from the header
|
2016-08-24 16:55:33 +00:00
|
|
|
match = jwtutil.TOKEN_REGEX.match(bearer_header)
|
2016-05-31 20:48:19 +00:00
|
|
|
if match is None:
|
|
|
|
raise InvalidBearerTokenException('Invalid bearer token format')
|
|
|
|
|
|
|
|
encoded_jwt = match.group(1)
|
|
|
|
logger.debug('encoded JWT: %s', encoded_jwt)
|
2016-09-19 20:19:29 +00:00
|
|
|
return decode_bearer_token(encoded_jwt, instance_keys, config)
|
2016-08-24 16:55:33 +00:00
|
|
|
|
|
|
|
|
2016-09-19 20:19:29 +00:00
|
|
|
def decode_bearer_token(bearer_token, instance_keys, config):
|
2016-08-24 16:55:33 +00:00
|
|
|
""" decode_bearer_token decodes the given bearer token that contains both a Key ID as well as the
|
|
|
|
encoded JWT and returns the decoded and validated JWT. On any error, raises an
|
|
|
|
InvalidBearerTokenException with the reason for failure.
|
|
|
|
"""
|
2016-05-31 20:48:19 +00:00
|
|
|
# Decode the key ID.
|
2016-08-24 16:55:33 +00:00
|
|
|
headers = jwt.get_unverified_header(bearer_token)
|
2016-05-31 20:48:19 +00:00
|
|
|
kid = headers.get('kid', None)
|
|
|
|
if kid is None:
|
2016-08-24 16:55:33 +00:00
|
|
|
logger.error('Missing kid header on encoded JWT: %s', bearer_token)
|
2016-05-31 20:48:19 +00:00
|
|
|
raise InvalidBearerTokenException('Missing kid header')
|
|
|
|
|
|
|
|
# Find the matching public key.
|
|
|
|
public_key = instance_keys.get_service_key_public_key(kid)
|
|
|
|
if public_key is None:
|
2017-09-12 20:19:55 +00:00
|
|
|
logger.error('Could not find requested service key %s with encoded JWT: %s', kid, bearer_token)
|
2016-05-31 20:48:19 +00:00
|
|
|
raise InvalidBearerTokenException('Unknown service key')
|
|
|
|
|
|
|
|
# Load the JWT returned.
|
|
|
|
try:
|
|
|
|
expected_issuer = instance_keys.service_name
|
2016-09-19 20:19:29 +00:00
|
|
|
audience = config['SERVER_HOSTNAME']
|
|
|
|
max_signed_s = config.get('REGISTRY_JWT_AUTH_MAX_FRESH_S', 3660)
|
2016-05-31 20:48:19 +00:00
|
|
|
max_exp = jwtutil.exp_max_s_option(max_signed_s)
|
2016-08-24 16:55:33 +00:00
|
|
|
payload = jwtutil.decode(bearer_token, public_key, algorithms=[ALGORITHM], audience=audience,
|
2016-06-27 18:17:15 +00:00
|
|
|
issuer=expected_issuer, options=max_exp, leeway=JWT_CLOCK_SKEW_SECONDS)
|
2016-05-31 20:48:19 +00:00
|
|
|
except jwtutil.InvalidTokenError as ite:
|
|
|
|
logger.exception('Invalid token reason: %s', ite)
|
|
|
|
raise InvalidBearerTokenException(ite)
|
|
|
|
|
|
|
|
if not 'sub' in payload:
|
|
|
|
raise InvalidBearerTokenException('Missing sub field in JWT')
|
|
|
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
def generate_bearer_token(audience, subject, context, access, lifetime_s, instance_keys):
|
|
|
|
""" Generates a registry bearer token (without the 'Bearer ' portion) based on the given
|
|
|
|
information.
|
2016-05-04 21:40:09 +00:00
|
|
|
"""
|
2016-05-31 20:48:19 +00:00
|
|
|
return _generate_jwt_object(audience, subject, context, access, lifetime_s,
|
|
|
|
instance_keys.service_name, instance_keys.local_key_id,
|
|
|
|
instance_keys.local_private_key)
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_jwt_object(audience, subject, context, access, lifetime_s, issuer, key_id,
|
|
|
|
private_key):
|
|
|
|
""" Generates a compact encoded JWT with the values specified. """
|
2016-05-04 21:40:09 +00:00
|
|
|
token_data = {
|
2016-05-31 20:48:19 +00:00
|
|
|
'iss': issuer,
|
2016-05-04 21:40:09 +00:00
|
|
|
'aud': audience,
|
2016-06-27 18:17:15 +00:00
|
|
|
'nbf': int(time.time()),
|
|
|
|
'iat': int(time.time()),
|
|
|
|
'exp': int(time.time() + lifetime_s),
|
2016-05-04 21:40:09 +00:00
|
|
|
'sub': subject,
|
|
|
|
'access': access,
|
|
|
|
'context': context,
|
|
|
|
}
|
|
|
|
|
|
|
|
token_headers = {
|
2016-05-31 20:48:19 +00:00
|
|
|
'kid': key_id,
|
2016-05-04 21:40:09 +00:00
|
|
|
}
|
|
|
|
|
2016-05-31 20:48:19 +00:00
|
|
|
return jwt.encode(token_data, private_key, ALGORITHM, headers=token_headers)
|
2016-05-04 21:40:09 +00:00
|
|
|
|
|
|
|
|
2017-03-22 17:19:22 +00:00
|
|
|
def build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=None):
|
2016-05-04 21:40:09 +00:00
|
|
|
""" Builds the custom context field for the JWT signed token and returns it,
|
|
|
|
along with the subject for the JWT signed token. """
|
2017-03-22 11:38:52 +00:00
|
|
|
|
2017-03-22 17:19:22 +00:00
|
|
|
# Default to quay root if not explicitly granted permission to see signer root
|
2017-03-22 11:38:52 +00:00
|
|
|
if not tuf_root:
|
2017-03-22 20:14:56 +00:00
|
|
|
tuf_root = QUAY_TUF_ROOT
|
2017-03-22 11:38:52 +00:00
|
|
|
|
2017-03-27 14:55:18 +00:00
|
|
|
context = {
|
|
|
|
CLAIM_TUF_ROOT: tuf_root
|
|
|
|
}
|
|
|
|
|
2016-05-04 21:40:09 +00:00
|
|
|
if oauthtoken:
|
2017-03-27 14:55:18 +00:00
|
|
|
context.update({
|
2016-05-04 21:40:09 +00:00
|
|
|
'kind': 'oauth',
|
|
|
|
'user': user.username,
|
|
|
|
'oauth': oauthtoken.uuid,
|
2017-03-27 14:55:18 +00:00
|
|
|
})
|
2016-05-04 21:40:09 +00:00
|
|
|
return (context, user.username)
|
|
|
|
|
|
|
|
if user:
|
2017-03-27 14:55:18 +00:00
|
|
|
context.update({
|
2016-05-04 21:40:09 +00:00
|
|
|
'kind': 'user',
|
|
|
|
'user': user.username,
|
2017-03-27 14:55:18 +00:00
|
|
|
})
|
2016-05-04 21:40:09 +00:00
|
|
|
return (context, user.username)
|
|
|
|
|
|
|
|
if token:
|
2017-03-27 14:55:18 +00:00
|
|
|
context.update({
|
2016-05-04 21:40:09 +00:00
|
|
|
'kind': 'token',
|
|
|
|
'token': token.code,
|
2017-03-27 14:55:18 +00:00
|
|
|
})
|
2016-05-04 21:40:09 +00:00
|
|
|
return (context, None)
|
|
|
|
|
2017-03-27 14:55:18 +00:00
|
|
|
context.update({
|
2016-05-04 21:40:09 +00:00
|
|
|
'kind': 'anonymous',
|
2017-03-27 14:55:18 +00:00
|
|
|
})
|
2016-05-04 21:40:09 +00:00
|
|
|
return (context, ANONYMOUS_SUB)
|
|
|
|
|
|
|
|
|