Fix and templatize the logic for external JWT AuthN and registry v2 Auth.
Make it explicit that the registry-v2 stuff is not ready for prime time.
This commit is contained in:
parent
768192927a
commit
bc29561f8f
11 changed files with 223 additions and 79 deletions
125
auth/jwt_auth.py
125
auth/jwt_auth.py
|
@ -8,11 +8,13 @@ from flask import request
|
|||
from flask.ext.principal import identity_changed, Identity
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cachetools import lru_cache
|
||||
|
||||
from app import app
|
||||
from auth_context import set_grant_user_context
|
||||
from permissions import repository_read_grant, repository_write_grant
|
||||
from util.names import parse_namespace_repository
|
||||
from util.http import abort
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -21,67 +23,88 @@ logger = logging.getLogger(__name__)
|
|||
TOKEN_REGEX = re.compile(r'Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)')
|
||||
|
||||
|
||||
class InvalidJWTException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
||||
""" Process a bearer token and return the loaded identity, or raise InvalidJWTException if an
|
||||
identity could not be loaded. Expects tokens and grants in the format of the Docker registry
|
||||
v2 auth spec: https://docs.docker.com/registry/spec/auth/token/
|
||||
"""
|
||||
logger.debug('Validating auth header: %s', bearer_token)
|
||||
|
||||
# Extract the jwt token from the header
|
||||
match = TOKEN_REGEX.match(bearer_token)
|
||||
if match is None or match.end() != len(bearer_token):
|
||||
raise InvalidJWTException('Invalid bearer token format')
|
||||
|
||||
encoded = match.group(1)
|
||||
logger.debug('encoded JWT: %s', encoded)
|
||||
|
||||
# Load the JWT returned.
|
||||
try:
|
||||
payload = jwt.decode(encoded, public_key, algorithms=['RS256'], audience='quay',
|
||||
issuer='token-issuer')
|
||||
except jwt.InvalidTokenError:
|
||||
raise InvalidJWTException('Invalid token')
|
||||
|
||||
if not 'sub' in payload:
|
||||
raise InvalidJWTException('Missing sub field in JWT')
|
||||
|
||||
if not 'exp' in payload:
|
||||
raise InvalidJWTException('Missing exp field in JWT')
|
||||
|
||||
# Verify that the expiration is no more than 300 seconds in the future.
|
||||
if datetime.fromtimestamp(payload['exp']) > datetime.utcnow() + timedelta(seconds=max_signed_s):
|
||||
raise InvalidJWTException('Token was signed for more than %s seconds' % max_signed_s)
|
||||
|
||||
username = payload['sub']
|
||||
loaded_identity = Identity(username, 'signed_jwt')
|
||||
|
||||
# Process the grants from the payload
|
||||
if 'access' in payload:
|
||||
for grant in payload['access']:
|
||||
if grant['type'] != 'repository':
|
||||
continue
|
||||
|
||||
namespace, repo_name = parse_namespace_repository(grant['name'])
|
||||
|
||||
if 'push' in grant['actions']:
|
||||
loaded_identity.provides.add(repository_write_grant(namespace, repo_name))
|
||||
elif 'pull' in grant['actions']:
|
||||
loaded_identity.provides.add(repository_read_grant(namespace, repo_name))
|
||||
|
||||
return loaded_identity
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_public_key(certificate_file_path):
|
||||
with open(certificate_file_path) as cert_file:
|
||||
cert_obj = load_pem_x509_certificate(cert_file.read(), default_backend())
|
||||
return cert_obj.public_key()
|
||||
|
||||
|
||||
def process_jwt_auth(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
logger.debug('Called with params: %s, %s', args, kwargs)
|
||||
auth = request.headers.get('authorization', '').strip()
|
||||
if auth:
|
||||
logger.debug('Validating auth header: %s', auth)
|
||||
max_signature_seconds = app.config.get('JWT_AUTH_MAX_FRESH_S', 300)
|
||||
certificate_file_path = app.config['JWT_AUTH_CERTIFICATE_PATH']
|
||||
public_key = load_public_key(certificate_file_path)
|
||||
|
||||
# Extract the jwt token from the header
|
||||
match = TOKEN_REGEX.match(auth)
|
||||
if match is None or match.end() != len(auth):
|
||||
logger.debug('Not a valid bearer token: %s', auth)
|
||||
return
|
||||
|
||||
encoded = match.group(1)
|
||||
logger.debug('encoded JWT: %s', encoded)
|
||||
|
||||
# Load the JWT returned.
|
||||
try:
|
||||
with open('/Users/jake/Projects/registry-v2/ca/quay.host.crt') as cert_file:
|
||||
cert_obj = load_pem_x509_certificate(cert_file.read(), default_backend())
|
||||
public_key = cert_obj.public_key()
|
||||
payload = jwt.decode(encoded, public_key, algorithms=['RS256'], audience='quay',
|
||||
issuer='token-issuer')
|
||||
except jwt.InvalidTokenError:
|
||||
logger.exception('Exception when decoding returned JWT')
|
||||
return (None, 'Invalid username or password')
|
||||
extracted_identity = identity_from_bearer_token(auth, max_signature_seconds, public_key)
|
||||
identity_changed.send(app, identity=extracted_identity)
|
||||
set_grant_user_context(extracted_identity.id)
|
||||
logger.debug('Identity changed to %s', extracted_identity.id)
|
||||
except InvalidJWTException as ije:
|
||||
abort(401, message=ije.message)
|
||||
|
||||
if not 'sub' in payload:
|
||||
raise Exception('Missing subject field in JWT')
|
||||
|
||||
if not 'exp' in payload:
|
||||
raise Exception('Missing exp field in JWT')
|
||||
|
||||
# Verify that the expiration is no more than 300 seconds in the future.
|
||||
if datetime.fromtimestamp(payload['exp']) > datetime.utcnow() + timedelta(seconds=300):
|
||||
logger.debug('Payload expiration is outside of the 300 second window: %s', payload['exp'])
|
||||
return (None, 'Invalid username or password')
|
||||
|
||||
username = payload['sub']
|
||||
loaded_identity = Identity(username, 'signed_jwt')
|
||||
|
||||
# Process the grants from the payload
|
||||
if 'access' in payload:
|
||||
for grant in payload['access']:
|
||||
if grant['type'] != 'repository':
|
||||
continue
|
||||
|
||||
namespace, repo_name = parse_namespace_repository(grant['name'])
|
||||
|
||||
if 'push' in grant['actions']:
|
||||
loaded_identity.provides.add(repository_write_grant(namespace, repo_name))
|
||||
elif 'pull' in grant['actions']:
|
||||
loaded_identity.provides.add(repository_read_grant(namespace, repo_name))
|
||||
|
||||
identity_changed.send(app, identity=loaded_identity)
|
||||
set_grant_user_context(username)
|
||||
|
||||
logger.debug('Identity changed to %s', username)
|
||||
else:
|
||||
logger.debug('No auth header.')
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
|
31
conf/selfsigned/jwt.crt
Normal file
31
conf/selfsigned/jwt.crt
Normal file
|
@ -0,0 +1,31 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
51
conf/selfsigned/jwt.key.insecure
Normal file
51
conf/selfsigned/jwt.key.insecure
Normal file
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -216,6 +216,11 @@ class DefaultConfig(object):
|
|||
# Signed registry grant token expiration in seconds
|
||||
SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull
|
||||
|
||||
# Registry v2 JWT Auth config
|
||||
JWT_AUTH_MAX_FRESH_S = 60 * 5 # At most the JWT can be signed for 300s in the future
|
||||
JWT_AUTH_CERTIFICATE_PATH = 'conf/selfsigned/jwt.crt'
|
||||
JWT_AUTH_PRIVATE_KEY_PATH = 'conf/selfsigned/jwt.key.insecure'
|
||||
|
||||
# The URL endpoint to which we redirect OAuth when generating a token locally.
|
||||
LOCAL_OAUTH_HANDLER = '/oauth/localapp'
|
||||
|
||||
|
|
|
@ -53,20 +53,22 @@ def _get_federated_user(username, email, federated_service, create_new_user):
|
|||
return (db_user, None)
|
||||
|
||||
|
||||
class JWTAuthUsers(object):
|
||||
class ExternalJWTAuthN(object):
|
||||
""" Delegates authentication to a REST endpoint that returns JWTs. """
|
||||
PUBLIC_KEY_FILENAME = 'jwt-authn.cert'
|
||||
|
||||
def __init__(self, verify_url, issuer, override_config_dir, http_client, public_key_path=None):
|
||||
def __init__(self, verify_url, issuer, override_config_dir, http_client, max_fresh_s,
|
||||
public_key_path=None):
|
||||
self.verify_url = verify_url
|
||||
self.issuer = issuer
|
||||
self.client = http_client
|
||||
self.max_fresh_s = max_fresh_s
|
||||
|
||||
default_key_path = os.path.join(override_config_dir, JWTAuthUsers.PUBLIC_KEY_FILENAME)
|
||||
default_key_path = os.path.join(override_config_dir, ExternalJWTAuthN.PUBLIC_KEY_FILENAME)
|
||||
public_key_path = public_key_path or default_key_path
|
||||
if not os.path.exists(public_key_path):
|
||||
error_message = ('JWT Authentication public key file "%s" not found in directory %s' %
|
||||
(JWTAuthUsers.PUBLIC_KEY_FILENAME, override_config_dir))
|
||||
(ExternalJWTAuthN.PUBLIC_KEY_FILENAME, override_config_dir))
|
||||
|
||||
raise Exception(error_message)
|
||||
|
||||
|
@ -102,10 +104,11 @@ class JWTAuthUsers(object):
|
|||
if not 'exp' in payload:
|
||||
raise Exception('Missing exp field in JWT')
|
||||
|
||||
# Verify that the expiration is no more than 300 seconds in the future.
|
||||
# Verify that the expiration is no more than self.max_fresh_s seconds in the future.
|
||||
expiration = datetime.utcfromtimestamp(payload['exp'])
|
||||
if expiration > datetime.utcnow() + timedelta(seconds=300):
|
||||
logger.debug('Payload expiration is outside of the 300 second window: %s', payload['exp'])
|
||||
if expiration > datetime.utcnow() + timedelta(seconds=self.max_fresh_s):
|
||||
logger.debug('Payload expiration is outside of the %s second window: %s', self.max_fresh_s,
|
||||
payload['exp'])
|
||||
return (None, 'Invalid username or password')
|
||||
|
||||
# Parse out the username and email.
|
||||
|
@ -330,7 +333,9 @@ class UserAuthentication(object):
|
|||
elif authentication_type == 'JWT':
|
||||
verify_url = app.config.get('JWT_VERIFY_ENDPOINT')
|
||||
issuer = app.config.get('JWT_AUTH_ISSUER')
|
||||
users = JWTAuthUsers(verify_url, issuer, override_config_dir, app.config['HTTPCLIENT'])
|
||||
max_fresh_s = app.config.get('JWT_AUTH_MAX_FRESH_S', 300)
|
||||
users = ExternalJWTAuthN(verify_url, issuer, override_config_dir, max_fresh_s,
|
||||
app.config['HTTPCLIENT'])
|
||||
else:
|
||||
raise RuntimeError('Unknown authentication type: %s' % authentication_type)
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# XXX This code is not yet ready to be run in production, and should remain disabled until such
|
||||
# XXX time as this notice is removed.
|
||||
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, make_response
|
||||
|
@ -25,7 +28,7 @@ def _require_repo_permission(permission_class, allow_public=False):
|
|||
permission = permission_class(namespace, repo_name)
|
||||
if (permission.can() or
|
||||
(allow_public and
|
||||
model.repository_is_public(namespace, repo_name))):
|
||||
model.repository.repository_is_public(namespace, repo_name))):
|
||||
return func(namespace, repo_name, *args, **kwargs)
|
||||
raise abort(401)
|
||||
return wrapped
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# XXX This code is not yet ready to be run in production, and should remain disabled until such
|
||||
# XXX time as this notice is removed.
|
||||
|
||||
import logging
|
||||
|
||||
from flask import make_response, url_for, request
|
||||
|
@ -14,8 +17,11 @@ from util.http import abort
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/<regex("' + digest_tools.DIGEST_PATTERN + '"):digest>',
|
||||
methods=['HEAD'])
|
||||
BASE_BLOB_ROUTE = '/<namespace>/<repo_name>/blobs/<regex("{0}"):digest>'
|
||||
BLOB_DIGEST_ROUTE = BASE_BLOB_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
||||
|
||||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||
@process_jwt_auth
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
|
@ -29,12 +35,12 @@ def check_blob_existence(namespace, repo_name, digest):
|
|||
abort(404)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/<regex("' + digest_tools.DIGEST_PATTERN + '"):digest>',
|
||||
methods=['GET'])
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
||||
@process_jwt_auth
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def download_blob(namespace, repo_name, digest):
|
||||
# TODO Implement this
|
||||
return make_response('')
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# XXX This code is not yet ready to be run in production, and should remain disabled until such
|
||||
# XXX time as this notice is removed.
|
||||
|
||||
import logging
|
||||
import re
|
||||
import jwt.utils
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
# XXX This code is not yet ready to be run in production, and should remain disabled until such
|
||||
# XXX time as this notice is removed.
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import jwt
|
||||
|
||||
from flask import request, jsonify, abort
|
||||
from cachetools import lru_cache
|
||||
|
||||
from app import app
|
||||
from data import model
|
||||
from auth.auth import process_auth
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
@ -23,10 +28,25 @@ SCOPE_REGEX = re.compile(
|
|||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_certificate_bytes(certificate_file_path):
|
||||
with open(certificate_file_path) as cert_file:
|
||||
return ''.join(cert_file.readlines()[1:-1]).rstrip('\n')
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_private_key(private_key_file_path):
|
||||
with open(private_key_file_path) as private_key_file:
|
||||
return private_key_file.read()
|
||||
|
||||
|
||||
@v2_bp.route('/auth')
|
||||
@process_auth
|
||||
@no_cache
|
||||
def generate_registry_jwt():
|
||||
""" This endpoint will generate a JWT conforming to the Docker registry v2 auth spec:
|
||||
https://docs.docker.com/registry/spec/auth/token/
|
||||
"""
|
||||
audience_param = request.args.get('service')
|
||||
logger.debug('Request audience: %s', audience_param)
|
||||
|
||||
|
@ -81,14 +101,12 @@ def generate_registry_jwt():
|
|||
'access': access,
|
||||
}
|
||||
|
||||
with open('/Users/jake/Projects/registry-v2/ca/quay.host.crt') as cert_file:
|
||||
certificate = ''.join(cert_file.readlines()[1:-1]).rstrip('\n')
|
||||
certificate = load_certificate_bytes(app.config['JWT_AUTH_CERTIFICATE_PATH'])
|
||||
|
||||
token_headers = {
|
||||
'x5c': [certificate],
|
||||
}
|
||||
|
||||
with open('/Users/jake/Projects/registry-v2/ca/quay.host.key.insecure') as private_key_file:
|
||||
private_key = private_key_file.read()
|
||||
private_key = load_private_key(app.config['JWT_AUTH_PRIVATE_KEY_PATH'])
|
||||
|
||||
return jsonify({'token':jwt.encode(token_data, private_key, 'RS256', headers=token_headers)})
|
||||
return jsonify({'token':jwt.encode(token_data, private_key, 'RS256', headers=token_headers)})
|
||||
|
|
|
@ -4,10 +4,10 @@ import jwt
|
|||
import base64
|
||||
|
||||
from app import app
|
||||
from flask import Flask, abort, jsonify, request, make_response
|
||||
from flask import Flask, jsonify, request, make_response
|
||||
from flask.ext.testing import LiveServerTestCase
|
||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||
from data.users import JWTAuthUsers
|
||||
from data.users import ExternalJWTAuthN
|
||||
from tempfile import NamedTemporaryFile
|
||||
from Crypto.PublicKey import RSA
|
||||
from datetime import datetime, timedelta
|
||||
|
@ -31,8 +31,8 @@ class JWTAuthTestCase(LiveServerTestCase):
|
|||
|
||||
def create_app(self):
|
||||
users = [
|
||||
{ 'name': 'cooluser', 'email': 'user@domain.com', 'password': 'password' },
|
||||
{ 'name': 'some.neat.user', 'email': 'neat@domain.com', 'password': 'foobar'}
|
||||
{'name': 'cooluser', 'email': 'user@domain.com', 'password': 'password'},
|
||||
{'name': 'some.neat.user', 'email': 'neat@domain.com', 'password': 'foobar'}
|
||||
]
|
||||
|
||||
jwt_app = Flask('testjwt')
|
||||
|
@ -82,10 +82,8 @@ class JWTAuthTestCase(LiveServerTestCase):
|
|||
|
||||
self.session = requests.Session()
|
||||
|
||||
self.jwt_auth = JWTAuthUsers(
|
||||
self.get_server_url() + '/user/verify',
|
||||
'authy', '', app.config['HTTPCLIENT'],
|
||||
JWTAuthTestCase.public_key.name)
|
||||
self.jwt_auth = ExternalJWTAuthN(self.get_server_url() + '/user/verify', 'authy', '',
|
||||
app.config['HTTPCLIENT'], 300, JWTAuthTestCase.public_key.name)
|
||||
|
||||
def tearDown(self):
|
||||
finished_database_for_testing(self)
|
|
@ -7,7 +7,7 @@ import OpenSSL
|
|||
import logging
|
||||
|
||||
from fnmatch import fnmatch
|
||||
from data.users import LDAPConnection, JWTAuthUsers, LDAPUsers
|
||||
from data.users import LDAPConnection, ExternalJWTAuthN, LDAPUsers
|
||||
from flask import Flask
|
||||
from flask.ext.mail import Mail, Message
|
||||
from data.database import validate_database_url, User
|
||||
|
@ -340,7 +340,8 @@ def _validate_jwt(config, password):
|
|||
|
||||
# Try to instatiate the JWT authentication mechanism. This will raise an exception if
|
||||
# the key cannot be found.
|
||||
users = JWTAuthUsers(verify_endpoint, issuer, OVERRIDE_CONFIG_DIRECTORY, app.config['HTTPCLIENT'])
|
||||
users = ExternalJWTAuthN(verify_endpoint, issuer, OVERRIDE_CONFIG_DIRECTORY,
|
||||
app.config['HTTPCLIENT'])
|
||||
|
||||
# Verify that the superuser exists. If not, raise an exception.
|
||||
username = get_authenticated_user().username
|
||||
|
|
Reference in a new issue