Commit 36eda630 authored by CAMPION Sebastien's avatar CAMPION Sebastien

JWT Token authentification

Complete documentation available at https://docs.docker.com/registry/spec/auth/token/
JWT specification https://jwt.io/
parent ef37531d
......@@ -110,6 +110,7 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [
'main',
'api.v1',
'jwt'
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS + list(
......
......@@ -11,6 +11,7 @@ urlpatterns = [
# Allgo stuff here
url(r'', include('main.urls')),
url(r'', include('jwt.urls')),
url(r'^accounts/', include('allauth.urls')),
url(r'^api/v1/', include('api.v1.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
......
import time
import hashlib
import base64
import subprocess
from Crypto.PublicKey import RSA
from jose import jwt
import config
SIGNING_KEY_PATH = config.env.ALLGO_TOKEN_SIGNING_KEY_PATH
SIGNING_KEY_TYPE = config.env.ALLGO_TOKEN_SIGNING_KEY_TYPE
SIGNING_KEY_ALG = config.env.ALLGO_TOKEN_SIGNING_KEY_ALG
SIGNING_KEY = open(SIGNING_KEY_PATH).read()
ISSUER = config.env.ALLGO_TOKEN_ISSUER
TOKEN_EXPIRATION = config.env.ALLGO_TOKEN_EXPIRATION
TOKEN_TYPE = config.env.ALLGO_TOKEN_TYPE
def run_command(command):
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return process.communicate()
def key_id_encode(the_bytes):
source = base64.b32encode(the_bytes)
result = []
for i in range(0, len(source), 4):
start = i
end = start+4
result.append(str(source[start:end], 'utf-8'))
return ":".join(result)
def kid_from_crypto_key(private_key_path, key_type):
"""
python implementation of
https://github.com/jlhawn/libtrust/blob/master/util.go#L192
returns a distinct identifier which is unique to
the public key derived from this private key.
The format generated by this library is a base32 encoding of a 240 bit
hash of the public key data divided into 12 groups like so:
ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP
"""
assert key_type == 'RSA', "Only RSA key type supported"
algorithm = hashlib.sha256()
key = RSA.importKey(open(private_key_path).read())
der = key.publickey().exportKey("DER")
algorithm.update(der)
return key_id_encode(algorithm.digest()[:30])
class Token(object):
def __init__(self, service, access_type="", access_name="", access_actions=None, subject=''):
if access_actions is None:
access_actions = []
self.issuer = ISSUER
self.signing_key = SIGNING_KEY
self.signing_key_path = SIGNING_KEY_PATH
self.signing_key_type = SIGNING_KEY_TYPE
self.signing_key_alg = SIGNING_KEY_ALG
self.token_expiration = TOKEN_EXPIRATION
self.token_type = TOKEN_TYPE
self.header = {
'typ': self.token_type,
'alg': self.signing_key_alg,
'kid': kid_from_crypto_key(self.signing_key_path, self.signing_key_type)
}
self.claim = {
'iss': self.issuer,
'sub': subject,
'aud': service,
'exp': int(time.time()) + int(self.token_expiration),
'nbf': int(time.time()) - 30,
'iat': int(time.time()),
'access': [
{
'type': access_type,
'name': access_name,
'actions': access_actions
}
]
}
def set_header(self, header):
self.header = header
def get_header(self):
return self.header
def set_claim(self, claim):
self.claim = claim
def get_claim(self):
return self.claim
def encode_token(self):
token = jwt.encode(self.claim, self.signing_key,
algorithm=self.signing_key_alg,
headers=self.header)
return token
from django.conf.urls import url
from . import views
app_name = 'jwt'
urlpatterns = [
url(r'^jwt/auth', views.jwt_auth, name="jwt_auth"), # REGISTRY_AUTH_TOKEN_REALM for docker registry
]
import base64
import logging
import config.env
from django.http import JsonResponse, HttpResponse
from main.models import User, AllgoUser, Runner
from .tokens import Token
log = logging.getLogger('jwt')
def jwt_auth(request):
"""
JWT auth used by docker registry endpoint specified at https://docs.docker.com/registry/spec/auth/jwt/
:param request:
:return:
"""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header:
log.info("Token request without http authorisation %s %s %s", request.META['HTTP_USER_AGENT'],
request.META['REMOTE_ADDR'], request.META['QUERY_STRING'])
return HttpResponse(status=401)
token_type, credentials = auth_header.split(' ')
username, password = base64.b64decode(credentials).decode('utf-8').split(':')
log.debug('HTTP_AUTHORIZATION %s username %s', auth_header, username)
if username == "$token" and Runner.objects.get(token=password):
log.info("Token for runner called")
user = Runner.objects.get(token=password)
else:
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
log.warning("Token request but user doest not exist")
return HttpResponse(status=401)
password_valid = user.check_password(password)
if token_type != 'Basic' or not password_valid:
log.info("Token request but user password mismatch")
return HttpResponse(status=401)
service = request.GET['service']
scope = request.GET['scope'] if 'scope' in request.GET.keys() else None
if not scope:
typ = ''
name = ''
actions = []
else:
params = scope.split(':')
if len(params) != 3:
return JsonResponse({'error': 'Invalid scope parameter'}, status=400)
typ = params[0]
name = params[1]
actions = params[2].split(',')
authorized_actions = get_allowed_actions(user, scope, actions, request) if scope else []
log.info("Token authorized actions %s %s %s", authorized_actions, user, scope)
token = Token(service, typ, name, authorized_actions)
encoded_token = token.encode_token()
return JsonResponse({'token': encoded_token})
def get_allowed_actions(user, scope, actions, request):
"""
Test if user is allowed to do actions in scope
To shorten it, if user is owner of the app, push/pull is allowed otherwise None
:param user:
:param scope:
:param actions:
:param request:
:return:
"""
resource_type, resource_name, resource_actions = scope.split(":")
if isinstance(user, Runner):
if user.account.is_superuser and get_client_ip(request) in config.env.ALLGO_ALLOWED_IP_ADMIN.split(','):
return ['*']
elif resource_type == "repository" and resource_name in [w.name for w in user.webapps]:
return ['pull']
else:
allgouser = AllgoUser.objects.get(user=user)
log.debug("Get allowed actions for user %s actions %s resource type %s", allgouser.user.username, actions,
resource_type)
if resource_type == "repository" and resource_name.rstrip('-incoming') in allgouser.getApp():
return actions
else:
log.error("User %s token %s can't access to scope %s (User apps : %s)", allgouser.user.username,
allgouser.token, scope, allgouser.getApp())
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
......@@ -72,7 +72,7 @@ services:
REGISTRY_HTTP_TLS_CERTIFICATE: "/certs/server.crt"
REGISTRY_HTTP_TLS_KEY: "/certs/server.key"
REGISTRY_AUTH: "token"
REGISTRY_AUTH_TOKEN_REALM: "http://0.0.0.0:8008/tokens"
REGISTRY_AUTH_TOKEN_REALM: "http://0.0.0.0:8008/jwt/token"
REGISTRY_AUTH_TOKEN_SERVICE: "allgo_registry"
REGISTRY_AUTH_TOKEN_ISSUER: "allgo_oauth"
REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: "/certs/server.crt"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment