Skip to content

Commit

Permalink
Refactor provider strings to global vars. Allow Auth0 validator to ad…
Browse files Browse the repository at this point in the history
…ditionally check the enabled backends directly. Remove apikey requirement from validate_token. Add regex whitelist functionality to return_to functionality in authorize endpoint, issue #26
  • Loading branch information
theferrit32 committed Oct 29, 2018
1 parent f3315a0 commit 8875d5a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 39 deletions.
4 changes: 2 additions & 2 deletions example/uwsgi.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[uwsgi]
module=microservice.wsgi:application
env=DJANGO_SETTINGS_MODULE=microservice.settings
module=auth_microservice.wsgi:application
env=DJANGO_SETTINGS_MODULE=auth_microservice.settings
master=true
pidfile=/tmp/auth_microservice-master.pid
processes=5
Expand Down
2 changes: 1 addition & 1 deletion token_service/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def get_admin_key(keylen=ADMIN_KEY_LEN):
if len(d) != raw_len:
logging.warn('admin key file %s must contain a %s byte hexidecimal string',
TOKEN_SERVICE_ADMIN_KEY, raw_len)
admin_key = d.encode('utf-8') # this remains as hex
admin_key = d # this remains as hex
tsc.admin_key = admin_key
logging_sensitive("read admin_key: %s", admin_key)
return admin_key
Expand Down
74 changes: 48 additions & 26 deletions token_service/redirect_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
STANDARD_OAUTH2 = 'OAuth 2.0'
SUPPORTED_STANDARDS = [STANDARD_OPENID_CONNECT, STANDARD_OAUTH2]
PROVIDER_GLOBUS = 'globus'

PROVIDER_GOOGLE = 'google'
PROVIDER_AUTH0 = 'auth0'
DEFAULT_PROVIDER = PROVIDER_AUTH0

def is_supported(provider):
return Config['providers'][provider]['standard'] in SUPPORTED_STANDARDS
Expand Down Expand Up @@ -127,9 +129,9 @@ def get_handler(request=None, token=None):
elif token:
provider = token.provider

if provider == 'globus':
if provider == PROVIDER_GLOBUS:
return GlobusRedirectHandler()
elif provider.startswith('auth0'):
elif provider.startswith(PROVIDER_AUTH0):
return Auth0RedirectHandler()
else:
return RedirectHandler()
Expand Down Expand Up @@ -161,13 +163,18 @@ def get_pending_by_field_one(fieldname, fieldval):
return pending[0]


def get_validator(provider=None):
if provider == 'google':
def get_validator(provider=DEFAULT_PROVIDER):
inv = RuntimeError('invalid provider {}'.format(str(provider)))
if not provider:
return Auth0Validator()
if provider == PROVIDER_GOOGLE:
return GoogleValidator()
elif provider.startswith('auth0'):
elif provider.startswith(PROVIDER_AUTH0):
return Auth0Validator()
elif provider == PROVIDER_GLOBUS:
return GlobusValidator()
else:
return Validator()
raise inv


def get_user(provider, sub, user_name=None, name=None, warn=True):
Expand Down Expand Up @@ -337,7 +344,7 @@ def accept(self, request):

if provider == PROVIDER_GLOBUS:
handler = GlobusRedirectHandler()
elif provider == 'auth0':
elif provider == PROVIDER_AUTH0:
handler = Auth0RedirectHandler()
else:
handler = self
Expand Down Expand Up @@ -577,12 +584,12 @@ class Auth0RedirectHandler(RedirectHandler):
IDTOKEN_USER_NAME = ['preferred_username', 'email', 'nickname']

def _generate_authorization_url(self, state, nonce, scopes, provider_tag):
if provider_tag != "auth0":
if provider_tag != PROVIDER_AUTH0:
raise RuntimeError('incorrect provider_tag in Auth0RedirectHandler._generate_authorization_url')
# This login field provides a Auth0 login UI and is specific to Auth0
endpoint = Config['providers']['auth0']['login_endpoint']
endpoint = Config['providers'][PROVIDER_AUTH0]['login_endpoint']
redirect_uri = Config['redirect_uri']
client_id = Config['providers']['auth0']['client_id']
client_id = Config['providers'][PROVIDER_AUTH0]['client_id']

scope = ' '.join(scopes)
scope = quote(scope)
Expand All @@ -608,8 +615,8 @@ def accept(self, request):
return HttpResponseBadRequest('callback request from login is malformed, or authorization session expired')
if now() > w.creation_time + datetime.timedelta(seconds=Config['url_expiration_timeout']):
return HttpResponseBadRequest('This authorization url has expired, please retry')
client_id = Config['providers']['auth0']['client_id']
client_secret = Config['providers']['auth0']['client_secret']
client_id = Config['providers'][PROVIDER_AUTH0]['client_id']
client_secret = Config['providers'][PROVIDER_AUTH0]['client_secret']
redirect_uri = Config['redirect_uri']
token_endpoint = 'https://heliumdatacommons.auth0.com/oauth/token'
token_response = self._token_request(
Expand Down Expand Up @@ -646,7 +653,7 @@ def _provider_sub_from_id_token(self, provider, id_token):
return provider, sub

def _refresh_token(self, token_model):
provider_config = Config['providers']['auth0']
provider_config = Config['providers'][PROVIDER_AUTH0]
if not token_model.refresh_token:
# don't rely on exception for this
raise RuntimeError('No refresh token available')
Expand Down Expand Up @@ -749,9 +756,11 @@ def _htb(_user, _nonce, _dict):

return (True, '', user, tokens[0], nonce)


"""
This is a reference implementation for OIDC IPDs which conform exactly to spec
"""
class Validator(object):
def validate(self, token, provider):
def validate(self, token, provider=DEFAULT_PROVIDER):
endpoint = get_provider_config(provider, 'introspection_endpoint')

creds = base64.b64encode('{}:{}'.format(
Expand Down Expand Up @@ -791,15 +800,16 @@ def validate(self, token, provider):
return r
return {'active': False}

class GlobusValidator(Validator):
def validate(self, token, provider=PROVIDER_GLOBUS):
return Validator().validate(token, provider)

class Auth0Validator(Validator):
def validate(self, token, provider='auth0'):
endpoint = Config['providers']['auth0']['userinfo_endpoint']
def validate(self, token, provider=PROVIDER_AUTH0):
endpoint = Config['providers'][PROVIDER_AUTH0]['userinfo_endpoint']
endpoint += '?access_token={}'.format(token)
response = requests.get(endpoint)
if response.status_code >= 300:
return {'active': False}
else:
if response.status_code < 300:
try:
body = json.loads(response.content.decode('utf-8'))
logging.debug('userinfo response: ' + str(body))
Expand All @@ -824,14 +834,26 @@ def validate(self, token, provider='auth0'):
r['username'] = body['email']
else:
# see if we recognize the sub
user = get_user(provider, r['sub'], warn=False)
if user:
r['username'] = user.user_name
try:
user = get_user(provider, r['sub'], warn=False)
if user:
r['username'] = user.user_name
except ObjectDoesNotExist:
pass
return r
else:
# token could not fetch userprofile from auth0
# check whether the access token is for a backend instead
for validator in [GlobusValidator, GoogleValidator]:
logging.debug('trying to validate against {} with token {}'.format(validator, token))
vresp = validator().validate(token)
if vresp['active'] == True:
return vresp
return {'active': False}


class GoogleValidator(Validator):
def validate(self, token, provider='google'):
def validate(self, token, provider=PROVIDER_GOOGLE):
ept = get_provider_config(provider, 'introspection_endpoint')
endpoint = '{}?access_token={}'.format(ept, token)

Expand All @@ -847,7 +869,7 @@ def validate(self, token, provider='google'):
except json.JSONDecodeError:
logging.warn('could not decode validate response: %s', content)
return {'active': False}
if int(body['expires_in']) > 0:
if int(body.get('expires_in', '0')) > 0:
r = {'active': True}
if body.get('user_id', None):
r['sub'] = body['user_id']
Expand Down
44 changes: 34 additions & 10 deletions token_service/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import re
import time
from six.moves.urllib.parse import urlparse
from django.http import (
HttpResponseBadRequest,
JsonResponse,
Expand Down Expand Up @@ -127,7 +128,7 @@ def prune_invalid(tokens):
else:
validator = redirect_handler.get_validator(t.provider)
validators[t.provider] = validator
validation_resp = validator.validate(t.access_token, t.provider)
validation_resp = validator.validate(t.access_token)
active = validation_resp.get('active', False)
# insert to worker cache
access_token_validation_cache[(t.access_token, t.provider)] = {
Expand Down Expand Up @@ -243,6 +244,32 @@ def _valid_api_key(request):
return False


def return_to_whitelisted(url):
if not Config.get('allow_return_regex'):
return False
else:
allowed = False
allowed_list = Config.get('allow_return_regex')
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
logging.warn('return_to url must be a valid http/https url, received: [{}]'.format(url))
return False
domain = parsed.netloc.split(':')[0]
for allowed_reg in allowed_list:
if not allowed_reg.startswith('^'):
allowed_reg = '^' + allowed_reg
if not allowed_reg.endswith('$'):
allowed_reg = allowed_reg + '$'
logging.debug('checking return_to domain [{}] against {}'.format(
domain, allowed_reg))
match = re.search(allowed_reg, domain)
if match is not None:
logging.debug('return_to [{}] matched [{}]'.format(url, allowed_reg))
allowed = True
else:
logging.debug('no match: ' + str(match))
return allowed

@require_http_methods(['GET'])
def url(request):
'''
Expand All @@ -266,9 +293,12 @@ def url(request):
if _valid_api_key(request) and return_to:
url, nonce = handler.add(None, scopes, provider, return_to)
else:
if return_to:
if return_to and return_to_whitelisted(return_to):
logging.debug('no apikey but return_to matched whitelist pattern')
url, nonce = handler.add(None, scopes, provider, return_to)
else:
logging.debug('invalid api key, ignoring return_to param')
url, nonce = handler.add(None, scopes, provider)
url, nonce = handler.add(None, scopes, provider)
return JsonResponse(status=200, data={'authorization_url': url, 'nonce': nonce})


Expand Down Expand Up @@ -339,9 +369,6 @@ def token(request):
except RuntimeError as e:
return JsonResponse(status=410, data={'msg': 'Token has expired'})

# if return_to:
# return _http_response(HttpResponseRedirect, util.build_redirect_url(return_to, token))
# else:
return JsonResponse(status=200, data={
'access_token': token.access_token,
'uid': token.user.sub,
Expand All @@ -368,15 +395,12 @@ def authcallback(request):
return red_resp


@require_valid_api_key
def validate_token(request):
if not _valid_api_key(request):
return _http_response(HttpResponseForbidden, 'must provide valid api key')
provider = request.GET.get('provider')
access_token = request.GET.get('access_token')
token_validator = redirect_handler.get_validator(provider)

validate_response = token_validator.validate(access_token, provider)
validate_response = token_validator.validate(access_token)
logging.debug('validate_response: %s', validate_response)
return JsonResponse(status=200, data=validate_response)

Expand Down

0 comments on commit 8875d5a

Please sign in to comment.