Skip to content

Commit

Permalink
Merge pull request #44 from indigo-dc/add-issuer-retriever
Browse files Browse the repository at this point in the history
Add issuer retriever
  • Loading branch information
marcvs committed Jul 15, 2021
2 parents 218b210 + ddd6a75 commit 82f5f5f
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 26 deletions.
84 changes: 68 additions & 16 deletions flaat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from . import tokentools
from . import issuertools
from . import flaat_exceptions
from .caches import Issuer_config_cache

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -113,7 +114,8 @@ def __init__(self):
self.client_id = None
self.client_secret = None
self.last_error = ''
self.issuer_configs = None
self.issuer_config_cache = Issuer_config_cache() # maps issuer to issuer configs # formerly issuer_configs
self.accesstoken_issuer_cache = {} # maps accesstoken to issuer
self.num_request_workers = 10
self.client_connect_timeout = 1.2 # seconds
# No leading slash ('/') in ops_that_support_jwt !!!
Expand All @@ -129,6 +131,8 @@ def __init__(self):
'https://login.helmholtz-data-federation.de/oauth2',
'https://login-dev.helmholtz.de/oauth2',
'https://login.helmholtz.de/oauth2',
'https://b2access.eudat.eu/oauth2',
'https://b2access-integration.fz-juelich.de/oauth2',
'https://services.humanbrainproject.eu/oidc',
'https://login.elixir-czech.org/oidc',
]
Expand Down Expand Up @@ -182,6 +186,10 @@ def set_trusted_OP_list(self, trusted_op_list):
self.trusted_op_list = []
for issuer in trusted_op_list:
self.trusted_op_list.append(issuer.rstrip('/'))

# iss_config = issuertools.find_issuer_config_in_list(self.trusted_op_list, self.op_hint,
# exclude_list = [])
# self.issuer_config_cache.add_list(iss_config)
def set_trusted_OP_file(self, filename='/etc/oidc-agent/issuer.config', hint=None):
'''Set filename of oidc-agent's issuer.config. Requires oidc-agent to be installed.'''
self.trusted_op_file = filename
Expand Down Expand Up @@ -269,7 +277,21 @@ def set_web_framework(self, framework_name):
logger.error("Specified Web Framework '%s' is not supported" % framework_name)
sys.exit (42)
def _find_issuer_config_everywhere(self, access_token):
'''Use many places to find issuer configs'''
'''Use many places to find issuer configs
'''

# 0: Use accesstoken_issuer cache to find issuerconfig:
if self.verbose > 0:
logger.info('0: Trying to find issuer in cache')
try:
issuer = self.accesstoken_issuer_cache[access_token]
iss_config = self.issuer_config_cache.get(issuer)
if self.verbose > 1:
logger.info(F" 0: returning {iss_config['issuer']}")
return [iss_config]
except KeyError as e:
# issuer not found in cache
pass

# 1: find info in the AT
if self.verbose > 0:
Expand Down Expand Up @@ -334,19 +356,40 @@ def get_info_thats_in_at(self, access_token):
# at_body = accesstoken_info['body']
# return (at_head, at_body)
return (accesstoken_info)
def get_issuer_from_accesstoken(self, access_token):
'''get the issuer that issued the accesstoken'''
try:
issuer = self.accesstoken_issuer_cache[access_token]
return(issuer)
except KeyError:
# update the accesstoken_issuer_cache:
self.get_info_from_userinfo_endpoints(access_token)
try:
issuer = self.accesstoken_issuer_cache[access_token]
return(issuer)
except KeyError:
return None

def get_info_from_userinfo_endpoints(self, access_token):
'''Traverse all reasonable configured userinfo endpoints and query them with the
access_token. Note: For OPs that include the iss inside the AT, they will be directly
queried, and are not included in the search (because that makes no sense).
Returns user_info object or None. If None is returned self.last_error is set with a
meaningful message.'''
meaningful message.
Also updates
- accesstoken_issuer_cache
- issuer_config_cache
'''
# user_info = "" # return value
user_info = None # return value

# get a sensible issuer config. In case we don't have a jwt AT, we poll more OPs
if self.issuer_configs is None:
self.issuer_configs = self._find_issuer_config_everywhere(access_token)
if self.issuer_configs is None or len(self.issuer_configs) == 0 :
issuer_config_list = self._find_issuer_config_everywhere(access_token)
self.issuer_config_cache.add_list(issuer_config_list)

# If there is no issuer in the cache by now, we're dead
if len(self.issuer_config_cache) == 0 :
logger.warning('No issuer config found, or issuer not supported')
return None

Expand Down Expand Up @@ -375,9 +418,10 @@ def safe_get(q):
t.daemon = True
t.start()

if self.verbose > 2:
logger.debug (F"len of issuer_configs: {len(self.issuer_configs)}")
for issuer_config in self.issuer_configs:
if self.verbose > 0:
logger.debug (F"len of issuer_config_cache: {len(self.issuer_config_cache)}")
for issuer_config in self.issuer_config_cache:
# logger.info(F"tyring to get userinfo from {issuer_config['issuer']}")
# user_info = issuertools.get_user_info(access_token, issuer_config)
params = {}
params['access_token'] = access_token
Expand All @@ -388,14 +432,21 @@ def safe_get(q):
result_q.join()
try:
while not result_q.empty():
user_info = result_q.get(block=False, timeout=self.client_connect_timeout)
if user_info is not None:
retval = result_q.get(block=False, timeout=self.client_connect_timeout)
if retval is not None:
(user_info, issuer_config) = retval
issuer = issuer_config['issuer']
if self.verbose > 1:
logger.debug(F"got issuer: {issuer}")
self.issuer_config_cache.add_config(issuer, issuer_config)
# logger.info(F"storing in accesstoken cache: {issuer} -=> {access_token}")
self.accesstoken_issuer_cache[access_token] = issuer
return (user_info)
except Empty:
logger.info("EMPTY result in thead join")
# pass
except Exception as e:
logger.info("Error: Uncaught Exception: {}".format(str(e)))
logger.error("Error: Uncaught Exception: {}".format(str(e)))
if user_info is None:
self.set_last_error ("User Info not found or not accessible. Something may be wrong with the Access Token.")
return(user_info)
Expand All @@ -404,13 +455,14 @@ def get_info_from_introspection_endpoints(self, access_token):
endpoint and return the info obtained from there'''
# get introspection_token
introspection_info = None
if self.issuer_configs is None:
self.issuer_configs = self._find_issuer_config_everywhere(access_token)
if self.issuer_configs is None:
issuer_config_list = self._find_issuer_config_everywhere(access_token)
self.issuer_config_cache.add_list(issuer_config_list)

if len(self.issuer_config_cache) == 0 :
logger.info("Issuer Configs yielded None")
self.set_last_error("Issuer of Access Token is not supported")
return None
for issuer_config in self.issuer_configs:
for issuer_config in self.issuer_config_cache:
introspection_info = issuertools.get_introspected_token_info(access_token, issuer_config,
self.client_id, self.client_secret)
if introspection_info is not None:
Expand Down
91 changes: 91 additions & 0 deletions flaat/caches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'''FLAsk support for OIDC Access Tokens -- FLAAT. A set of decorators for authorising
access to OIDC authenticated REST APIs.'''
# This code is distributed under the MIT License
# pylint
# vim: tw=100 foldmethod=indent
# pylint: disable=invalid-name, superfluous-parens
# pylint: disable=logging-not-lazy, logging-format-interpolation, logging-fstring-interpolation
# pylint: disable=wrong-import-position, no-self-use, line-too-long

import logging
import logsetup
logger = logging.getLogger(__name__)

class Issuer_config_cache():
'''Caching of issuer configs'''
def __init__(self):
self.entries = {} # maps 'iss' to the whole issuer config
self.n = 0
def add_config(self, iss, issuer_config):
'''add entry'''
# if self.get(iss) is not None:
# logger.info(F"updating: {iss}")
# else:
# logger.info(F"adding: {iss}")
self.entries[issuer_config['issuer']] = issuer_config
def add_list(self, issuer_configs):
'''add entry'''
for issuer_config in issuer_configs:
self.add_config(issuer_config['issuer'], issuer_config)
def get(self, iss):
'''get entry'''
try:
return (self.entries[iss])
except KeyError:
# logger.warning(F"cannot return issuer config for {iss}")
return None
def remove(self, iss):
'''remove entry'''
del self.entries[iss]
def dump_to_log(self):
'''display cache'''
logger.info("Issuer Cache:")
for iss in self.entries:
logger.info(F" {self.entries[iss]['issuer']:30} -> {self.entries[iss]['userinfo_endpoint']}")

def __iter__(self):
self.n = 0
return self

def __next__(self):
my_length = len(self.entries.keys())
if self.n < my_length:
retval = self.entries[list(self.entries.keys())[self.n]]
self.n += 1
return retval
raise StopIteration

def __len__(self):
'''return length'''
return len(self.entries.keys())

def has(self, iss):
'''do we have an entry'''
if iss in self.entries.keys():
return True
return False


if __name__ == '__main__':
ic = Issuer_config_cache()
print (F"is none: {ic is None}")
ic.add_config('first_issuer1', {'issuer': 'first issuer1', 'userinfo_endpoint': 'userinfo1'})
ic.add_config('sencodnd issuer2', {'issuer': 'sencodnd issuer2', 'userinfo_endpoint': 'userinfo2'})

print ('--')
print (F"test: {ic.get('test2')}")
print ('--')

# ic.dump_to_log()

for x in ic:
print(F"iterating: {x}")

print(F"length: {len(ic)}")

print(F"testing in")
if ic.has('first issuer1'):
print ("Yes")
else:
print ("NOPE")

25 changes: 15 additions & 10 deletions flaat/issuertools.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ def find_issuer_config_in_list(op_list, op_hint = None, exclude_list = []):
# if verbose>1:
# logger.info('Considering issuer %s' % issuer)
if issuer in exclude_list:
if verbose>1:
logger.info('skipping %s due to exclude list' % issuer)
if verbose > 1:
logger.debug('skipping %s due to exclude list' % issuer)
continue
issuer_wellknown=issuer + '/.well-known/openid-configuration'
if op_hint is None:
Expand Down Expand Up @@ -197,7 +197,7 @@ def get_iss_config_from_endpoint(issuer_url):
if resp.status_code != 200:
logger.warning ('Getconfig: resp: %s' % resp.status_code)
except requests.exceptions.ConnectionError as e:
if verbose > 1:
if verbose > 2:
logger.warning ('Warning: cannot obtain iss_config from endpoint: {}'.format(config_url))
# print ('Additional info: {}'.format (e))
return None
Expand All @@ -220,30 +220,35 @@ def get_user_info(access_token, issuer_config):
if verbose > 2:
logger.debug('using this access token: %s' % access_token)
if verbose > 0:
logger.info('Getting userinfo from %s' % issuer_config['userinfo_endpoint'])
logger.info('Trying to get userinfo from %s' % issuer_config['userinfo_endpoint'])
try:
resp = requests.get (issuer_config['userinfo_endpoint'], verify=verify_tls, headers=headers,
resp = requests.get(issuer_config['userinfo_endpoint'], verify=verify_tls, headers=headers,
timeout=timeout)
except requests.exceptions.ReadTimeout:
logger.error("ReadTimeout caught for issuer_config['userinfo_endpoint']")
logger.debug(F"headers were: {headers}, timeout: {timeout}")
logger.error("ReadTimeout caught for issuer_config['issuer']")
# logger.debug(F"headers were: {headers}, timeout: {timeout}")
return None
if resp.status_code != 200:
if verbose > 2:
logger.warning('Error getting userinfo from %s: %s / %s / %s' % (
if verbose > 1:
logger.warning('Not getting userinfo from %s: %s / %s / %s' % (
issuer_config['userinfo_endpoint'],
resp.status_code,
resp.text,
resp.reason))
if verbose > 2:
logger.warning(F"request was: get {issuer_config['userinfo_endpoint']}")
logger.warning(F"headers were: {headers}, timeout: {timeout}")
# return ({'error': '{}: {}'.format(resp.status_code, resp.reason)})
return None

if verbose > 0:
logger.info(' got userinfo from %s' % issuer_config['userinfo_endpoint'])
resp_json=resp.json()
if verbose > 2:
logger.info("Actual Userinfo: from %s" % issuer_config['userinfo_endpoint'] + ": "+ json.dumps(resp_json, sort_keys=True, indent=4, separators=(',', ': ')))
if resp.status_code != 200:
logger.info('userinfo: resp: %s' % resp.status_code)
return resp_json
return (resp_json, issuer_config)

def get_introspected_token_info(access_token, issuer_config, client_id=None, client_secret=None):
'''Query te token introspection endpoint, if there is a client_id and client_secret set'''
Expand Down
1 change: 1 addition & 0 deletions flaat/logsetup.py

0 comments on commit 82f5f5f

Please sign in to comment.