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:
Jake Moshenko 2015-07-16 15:49:06 -04:00
parent 768192927a
commit bc29561f8f
11 changed files with 223 additions and 79 deletions

View file

@ -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
View file

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFXDCCA0agAwIBAgIBAjALBgkqhkiG9w0BAQswLTEMMAoGA1UEBhMDVVNBMRAw
DgYDVQQKEwdldGNkLWNhMQswCQYDVQQLEwJDQTAeFw0xNTA3MTYxOTQzMTdaFw0y
NTA3MTYxOTQzMTlaMEYxDDAKBgNVBAYTA1VTQTEQMA4GA1UEChMHZXRjZC1jYTEQ
MA4GA1UECxMHand0YXV0aDESMBAGA1UEAxMJMTI3LjAuMC4xMIICIjANBgkqhkiG
9w0BAQEFAAOCAg8AMIICCgKCAgEAs5RxPVfO7iPZnFIP0DPiiMMMykDEG0OV6O1x
QycVReI2ELIPiWqfDFVcn6XXI/0kpvNeLGr2dDXaQFZYz+rNVDYBjM3djvibFhwa
30URmfHI9iZM703zdMZwc07+TIteIj1Q4MYhbPB4f6oERtLO29RffN9KH2FQvtzx
CF/GFb6vcHOeCeKZEGjxbQ2vfhMJh+UiO6woBooAJULBaM9hxErszqWqu0QKcV2h
NaW6fSf6aVUbFTu9hhYfkujDBR5EmwVFcKxUF+AHDrAshR/VdTHb0SJ3OtKz0vGv
NCc844J8nhUg7SeeO6ONeAq6cDRN65eJ7nJC1Nhhq2DpOgNxu+j0Dz7F+EEtNWpE
ezGjbRjmM4Ekhvsa/SUdzubInrnyHFYcbMZZIZzbgAJfruZHVKWWXjbxyG74xix+
+KzBs9jkCHSNNWnXTx3dev4dp4QltZ048crA1lioim8/W5GzYjvkfNwx6OohC4yD
5UoblQsY5vDdJ+S8g4feTmJMoNHdS/4ar/sVojUDX3KOF3bCZ6w4Ufx09EBXeUlQ
9gzs63xAvFhGk8anFSQbRoQgoKoivHpzlANquhWvRZCDtW5P4RLaHcOLjhq6nwe6
WW+vtDgEEKzdSj1We6grDPoT1kTagJ0gvpX+jcesu5d0e8MHt+qu0WTJwvCxcI+r
8zhXX/MCAwEAAaNyMHAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0G
A1UdDgQWBBQqTEeoqfZjPwzZYdkktdV+3Pl6SDAfBgNVHSMEGDAWgBRXeieifbQ2
jgPj8wwrrixidWU9tzAPBgNVHREECDAGhwR/AAABMAsGCSqGSIb3DQEBCwOCAgEA
KIFrN5mDOvbBEWYZSnyrGkF4bsXCbmLtg3FJkgdn5vJ916/2WtgceJMQKu1KE90V
giaRaSm4HEb63VBrCNdvoU0ZSRO/xQI2SqbERdFa36w31W71D16qDchRsFz+dEt4
7jm1gIdl/UYCMqFye7t+6H/w4J8p1M90RlSXw8wHyFEPOjEfBOM5uSpO9xsXW04+
DpfJvsLmvhaaE+OUrPft+VTtf0Wc7vV8jfS21D9nB/CJVaoS71m3FEHD8BlTZIqB
qcU67UJc7qhUJ3HyKbpJgFQcvEQ8GL+PJnsCO7Y/zCCbYLwjV1GffvHMGQ2JAJbB
2qnUxPqVmP87X3YDMXPVubW+CtoRPz7BIYsX2/HejlYOtlT25+SrHwpXRT5lcgbt
a9dcHhUmNNpfTgZpbPrPfdzqw+ze+HcbJAECWgm8v10quGbP5NZCnySM7LIJ8p7C
dLOGGuZnUaruqA3FRYS3147bdhGF1gLwGuM+BwzzvoppMf5kZuBWq6j6Feg1I68z
n1qhlEJSMoS1qUEq/8oXYgSs2ttvMAhZ4CqKPZztp3oZLPzZgL/eKb4JEjhpgitJ
TrgLFwAytHGZIWke/lR+Ca9qo/uMebduLu6akqZ5yrxl/DuHcBV8KGq+rXJIvxxj
O9hZBNQ+WDPvQlSN2z/An17zZePLgxspjZXIkkgSg1Y=
-----END CERTIFICATE-----

View file

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View file

@ -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'

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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)})

View file

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

View file

@ -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