Add support for Dex to Quay

Fixes #306

- Adds support for Dex as an OAuth external login provider
- Adds support for OIDC in general
- Extract out external logins on the JS side into a service
- Add a feature flag for disabling direct login
- Add support for directing to the single external login service
- Does *not* yet support the config in the superuser tool
This commit is contained in:
Joseph Schorr 2015-09-04 16:14:46 -04:00
parent 46f150cafb
commit c0286d1ac3
27 changed files with 533 additions and 176 deletions

View file

@ -1,5 +1,13 @@
import urlparse
import github
import json
import logging
import time
from cachetools.func import TTLCache
from jwkest.jwk import KEYS, keyrep
logger = logging.getLogger(__name__)
class OAuthConfig(object):
def __init__(self, config, key_name):
@ -38,10 +46,8 @@ class OAuthConfig(object):
def exchange_code_for_token(self, app_config, http_client, code, form_encode=False,
redirect_suffix=''):
redirect_suffix='', client_auth=False):
payload = {
'client_id': self.client_id(),
'client_secret': self.client_secret(),
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': self.get_redirect_uri(app_config, redirect_suffix)
@ -51,11 +57,18 @@ class OAuthConfig(object):
'Accept': 'application/json'
}
auth = None
if client_auth:
auth = (self.client_id(), self.client_secret())
else:
payload['client_id'] = self.client_id()
payload['client_secret'] = self.client_secret()
token_url = self.token_endpoint()
if form_encode:
get_access_token = http_client.post(token_url, data=payload, headers=headers)
get_access_token = http_client.post(token_url, data=payload, headers=headers, auth=auth)
else:
get_access_token = http_client.post(token_url, params=payload, headers=headers)
get_access_token = http_client.post(token_url, params=payload, headers=headers, auth=auth)
json_data = get_access_token.json()
if not json_data:
@ -248,3 +261,102 @@ class GitLabOAuthConfig(OAuthConfig):
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
'GITLAB_ENDPOINT': self._endpoint(),
}
OIDC_WELLKNOWN = ".well-known/openid-configuration"
PUBLIC_KEY_CACHE_TTL = 3600 # 1 hour
class OIDCConfig(OAuthConfig):
def __init__(self, config, key_name):
super(OIDCConfig, self).__init__(config, key_name)
self._public_key_cache = TTLCache(1, PUBLIC_KEY_CACHE_TTL, missing=self._get_public_key)
self._oidc_config = {}
self._http_client = config['HTTPCLIENT']
if self.config.get('OIDC_SERVER'):
self._load_via_discovery(config['DEBUGGING'])
def _load_via_discovery(self, is_debugging):
oidc_server = self.config['OIDC_SERVER']
if not oidc_server.startswith('https://') and not is_debugging:
raise Exception('OIDC server must be accessed over SSL')
discovery_url = urlparse.urljoin(oidc_server, OIDC_WELLKNOWN)
discovery = self._http_client.get(discovery_url, timeout=5)
if discovery.status_code / 100 != 2:
raise Exception("Could not load OIDC discovery information")
try:
self._oidc_config = json.loads(discovery.text)
except ValueError:
logger.exception('Could not parse OIDC discovery for url: %s', discovery_url)
raise Exception("Could not parse OIDC discovery information")
def authorize_endpoint(self):
return self._oidc_config['authorization_endpoint'] + '?'
def token_endpoint(self):
return self._oidc_config['token_endpoint']
def user_endpoint(self):
return None
def validate_client_id_and_secret(self, http_client, app_config):
pass
def get_public_config(self):
return {
'CLIENT_ID': self.client_id(),
'AUTHORIZE_ENDPOINT': self.authorize_endpoint()
}
@property
def issuer(self):
return self.config.get('OIDC_ISSUER', self.config['OIDC_SERVER'])
def get_public_key(self, force_refresh=False):
""" Retrieves the public key for this handler. """
# If force_refresh is true, we expire all the items in the cache by setting the time to
# the current time + the expiration TTL.
if force_refresh:
self._public_key_cache.expire(time=time.time() + PUBLIC_KEY_CACHE_TTL)
# Retrieve the public key from the cache. If the cache does not contain the public key,
# it will internally call _get_public_key to retrieve it and then save it. The None is
# a random key chose to be stored in the cache, and could be anything.
return self._public_key_cache[None]
def _get_public_key(self):
""" Retrieves the public key for this handler. """
keys_url = self._oidc_config['jwks_uri']
keys = KEYS()
keys.load_from_url(keys_url)
if not list(keys):
raise Exception('No keys provided by OIDC provider')
rsa_key = list(keys)[0]
rsa_key.deserialize()
return rsa_key.key.exportKey('PEM')
class DexOAuthConfig(OIDCConfig):
def service_name(self):
return 'Dex'
@property
def public_title(self):
return self.get_public_config()['OIDC_TITLE']
def get_public_config(self):
return {
'CLIENT_ID': self.client_id(),
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
# TODO(jschorr): This should ideally come from the Dex side.
'OIDC_TITLE': 'Dex',
'OIDC_LOGO': 'https://tectonic.com/assets/ico/favicon-96x96.png'
}