Some fixes and tests for v2 auth

Fixes #395
This commit is contained in:
Jake Moshenko 2015-09-10 12:24:33 -04:00
parent 5029ab62f6
commit 9c3ddf846f
7 changed files with 290 additions and 35 deletions

View file

@ -1,7 +1,7 @@
import logging
import re
from datetime import datetime, timedelta
from jsonschema import validate, ValidationError
from functools import wraps
from flask import request
from flask.ext.principal import identity_changed, Identity
@ -20,7 +20,45 @@ from util.security import strictjwt
logger = logging.getLogger(__name__)
TOKEN_REGEX = re.compile(r'Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)')
TOKEN_REGEX = re.compile(r'^Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)$')
ACCESS_SCHEMA = {
'type': 'array',
'description': 'List of access granted to the subject',
'items': {
'type': 'object',
'required': [
'type',
'name',
'actions',
],
'properties': {
'type': {
'type': 'string',
'description': 'We only allow repository permissions',
'enum': [
'repository',
],
},
'name': {
'type': 'string',
'description': 'The name of the repository for which we are receiving access'
},
'actions': {
'type': 'array',
'description': 'List of specific verbs which can be performed against repository',
'items': {
'type': 'string',
'enum': [
'push',
'pull',
],
},
},
},
},
}
class InvalidJWTException(Exception):
@ -36,7 +74,7 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
# Extract the jwt token from the header
match = TOKEN_REGEX.match(bearer_token)
if match is None or match.end() != len(bearer_token):
if match is None:
raise InvalidJWTException('Invalid bearer token format')
encoded = match.group(1)
@ -44,27 +82,31 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
# Load the JWT returned.
try:
payload = strictjwt.decode(encoded, public_key, algorithms=['RS256'], audience='quay',
issuer='token-issuer')
expected_issuer = app.config['JWT_AUTH_TOKEN_ISSUER']
audience = app.config['SERVER_HOSTNAME']
max_exp = strictjwt.exp_max_s_option(max_signed_s)
payload = strictjwt.decode(encoded, public_key, algorithms=['RS256'], audience=audience,
issuer=expected_issuer, options=max_exp)
except strictjwt.InvalidTokenError:
logger.exception('Invalid token reason')
raise InvalidJWTException('Invalid token')
if not 'sub' in payload:
raise InvalidJWTException('Missing sub 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
if 'access' in payload:
try:
validate(payload['access'], ACCESS_SCHEMA)
except ValidationError:
logger.exception('We should not be minting invalid credentials')
raise InvalidJWTException('Token contained invalid or malformed access grants')
for grant in payload['access']:
namespace, repo_name = parse_namespace_repository(grant['name'])
if 'push' in grant['actions']:
@ -88,7 +130,7 @@ def process_jwt_auth(func):
logger.debug('Called with params: %s, %s', args, kwargs)
auth = request.headers.get('authorization', '').strip()
if auth:
max_signature_seconds = app.config.get('JWT_AUTH_MAX_FRESH_S', 300)
max_signature_seconds = app.config.get('JWT_AUTH_MAX_FRESH_S', 3660)
certificate_file_path = app.config['JWT_AUTH_CERTIFICATE_PATH']
public_key = load_public_key(certificate_file_path)