diff --git a/src/microservice/token_service/crypt.py b/src/microservice/token_service/crypt.py index 028b097..7d95afd 100644 --- a/src/microservice/token_service/crypt.py +++ b/src/microservice/token_service/crypt.py @@ -18,30 +18,30 @@ def encrypt(self, plaintext): pad_n = (AES.block_size - (len(plaintext) % AES.block_size)) plaintext += pad_n * chr(pad_n) - print('crypt.encrypt plaintext: ' + str(plaintext)) + #print('crypt.encrypt plaintext: ' + str(plaintext)) iv = self.random(AES.block_size) - print('crypt.encrypt iv: ' + str(iv)) + #print('crypt.encrypt iv: ' + str(iv)) aes = AES.new(self.key, AES.MODE_CFB, iv) encr = aes.encrypt(plaintext) enco = base64.b64encode(iv + encr) enco = enco.decode('utf-8') - print('crypt.encrypt enco: ' + str(enco)) - print("encrypted [{}] to [{}]".format(plaintext,enco)) + #print('crypt.encrypt enco: ' + str(enco)) + #print("encrypted [{}] to [{}]".format(plaintext,enco)) return enco def decrypt(self, ciphertext): - print('crypt.decrypt ciphertext: ' + str(ciphertext)) + #print('crypt.decrypt ciphertext: ' + str(ciphertext)) if len(ciphertext) % 4 != 0: ciphertext += '=' * (4 - (len(ciphertext) % 4)) de_enco = base64.b64decode(ciphertext) iv = de_enco[:AES.block_size] aes = AES.new(self.key, AES.MODE_CFB, iv) de_encr = aes.decrypt(de_enco[AES.block_size:]) - print('crypt.decrypt de_encr: ' + str(de_encr)) + #print('crypt.decrypt de_encr: ' + str(de_encr)) # unpad pad_n = de_encr[-1] de_encr = de_encr[:-pad_n] de_encr = de_encr.decode('utf-8') - print('crypt.decrypt de_encr unpad: ' + str(de_encr)) + #print('crypt.decrypt de_encr unpad: ' + str(de_encr)) return de_encr diff --git a/src/microservice/token_service/models.py b/src/microservice/token_service/models.py index 903edc3..8c5c61f 100644 --- a/src/microservice/token_service/models.py +++ b/src/microservice/token_service/models.py @@ -11,16 +11,16 @@ def __init__(self, *args, **kwargs): # invoked to convert db value to python value def from_db_value(self, value, expression, connection): - print('EncryptedTextField.from_db_value value: ' + str(value)) + #print('EncryptedTextField.from_db_value value: ' + str(value)) dec = self.crypt.decrypt(value) - print('EncryptedTextField.from_db_value({}) -> {}'.format(value, dec)) + #print('EncryptedTextField.from_db_value({}) -> {}'.format(value, dec)) return dec # invoked before saving python value to db value def get_prep_value(self, value): - print('EncryptedTextField.get_prep_value value: ' + str(value)) + #print('EncryptedTextField.get_prep_value value: ' + str(value)) enc = self.crypt.encrypt(value) - print('EncryptedTextField.get_prep_value({}) -> {}'.format(value, enc)) + #print('EncryptedTextField.get_prep_value({}) -> {}'.format(value, enc)) return enc class User(models.Model): @@ -36,6 +36,7 @@ class Token(models.Model): issuer = models.CharField(max_length=256) enabled = models.BooleanField(default=True) scopes = models.ManyToManyField('Scope') + nonce = models.CharField(max_length=256) class Scope(models.Model): name = models.CharField(max_length=256) diff --git a/src/microservice/token_service/redirect_handler.py b/src/microservice/token_service/redirect_handler.py index 486d0a3..05902c3 100644 --- a/src/microservice/token_service/redirect_handler.py +++ b/src/microservice/token_service/redirect_handler.py @@ -76,6 +76,7 @@ def __init__(self, config=None): ''' @mutex(RedirectState.waiting_lock) def add(self, uid, scopes, provider_tag): + print('adding callback waiter with uid {}, scopes {}, provider {}'.format(uid, scopes, provider_tag)) scopes = sorted(scopes) # prevent unlikely chance that two waiting authorizations have same nonce or state # TODO change this to have in-memory cache of old nonce and state values. keep for N days @@ -108,21 +109,45 @@ def add(self, uid, scopes, provider_tag): @mutex(RedirectState.blocking_lock) def block(self, uid, scopes, provider_tag): scopes = sorted(scopes) - w = [x for x in RedirectState.waiting if x['uid'] == uid and sorted(x['scopes']) == sorted(scopes) and x['provider'] == provider_tag] + w = [x for x in RedirectState.waiting if x['uid'] == uid and list_subset(scopes, x['scopes']) and x['provider'] == provider_tag] #if len(w) == 0: # return None # no pending callback found, must re-authorize from scratch by calling add - b = [x for x in RedirectState.blocking if x['uid'] == uid and sorted(x['scopes']) == sorted(scopes) and x['provider'] == provider_tag] + b = [x for x in RedirectState.blocking if x['uid'] == uid and list_subset(scopes, x['scopes']) and x['provider'] == provider_tag] if len(b) > 0: b = b[0] return b['lock'] else: lock = Condition() - RedirectState.blocking.append({ + observer = { 'uid': uid, 'scopes': scopes, 'provider': provider_tag, - 'lock': lock - }) + 'lock': lock, + 'nonce': b['nonce'] + } + RedirectState.blocking.append(observer) + return lock + + @mutex(RedirectState.blocking_lock) + def block_nonce(self, nonce): + w = [x for x in RedirectState.waiting if x['nonce'] == nonce] + if len(w) == 0: + return None # no pending callback found, must re-authorize from scratch by calling add + w = w[0] + b = [x for x in RedirectState.blocking if x['nonce'] == nonce] + if len(b) > 0: + b = b[0] + return b['lock'] + else: + lock = Condition() + observer = { + 'uid': w['uid'], + 'scopes': w['scopes'], + 'provider': w['provider'], + 'lock': lock, + 'nonce': nonce + } + RedirectState.blocking.append(observer) return lock @@ -176,6 +201,7 @@ def accept(self, request): print('token_response:\n' + str(body)) # convert expires_in to timestamp expire_time = datetime.datetime.now() + datetime.timedelta(seconds=expires_in) + expire_time = expire_time.replace(tzinfo=datetime.timezone.utc) # expand the id_token to the encoded json object # TODO signature validation if signature provided @@ -208,11 +234,12 @@ def accept(self, request): token = models.Token( user=user, access_token=access_token, - refresh_token=refresh_token,#TODO what if no refresh_token in response + refresh_token=refresh_token, #TODO what if no refresh_token in response expires=expire_time, provider=provider, issuer=issuer, - enabled=True + enabled=True, + nonce=nonce ) token.save() @@ -235,12 +262,22 @@ def accept(self, request): scopes = models.ManyToManyField('Scope') ''' - # notify anyone blocking for this token criteria + # notify anyone blocking for (uid,scopes,provider) token criteria with RedirectState.blocking_lock: - b = [x for x in RedirectState.blocking if x['uid'] == sub and sorted(x['scopes']) == sorted(scopes) and x['provider'] == provider_tag] + b = [x for x in RedirectState.blocking if x['uid'] == sub and list_subset(w['scopes'], x['scopes']) and x['provider'] == provider] # should only be one which matches, but just in case... for observer in b: - b['lock'].notify_all() + with observer['lock']: + #observer['access_token'] = access_token + observer['lock'].notify_all() + RedirectState.blocking.remove(observer) + + # notify anyone blocking on the nonce + with RedirectState.blocking_lock: + b = [x for x in RedirectState.blocking if x['nonce'] == nonce] + for observer in b: + with observer['lock']: + observer['lock'].notify_all() RedirectState.blocking.remove(observer) return HttpResponse('Successfully authenticated user') @@ -348,3 +385,11 @@ def _generate_authorization_url(self, state, nonce, scopes, provider_tag): additional_params, ) return url + +def list_subset(A, B): + if not A or not B: + return False + for a in A: + if a not in B: + return False + return True diff --git a/src/microservice/token_service/urls.py b/src/microservice/token_service/urls.py index 95c958d..ea11559 100644 --- a/src/microservice/token_service/urls.py +++ b/src/microservice/token_service/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ path('admin/key', views.create_key, name='create_key'), path('token', views.token, name='token'), + #path('tokenbynonce', views.token_by_nonce, name='token_by_url'), path('authcallback', views.authcallback, name='authcallback'), ] diff --git a/src/microservice/token_service/views.py b/src/microservice/token_service/views.py index 516f007..29e9f5f 100644 --- a/src/microservice/token_service/views.py +++ b/src/microservice/token_service/views.py @@ -53,7 +53,9 @@ def _thread_block(lock, timeout): def _valid_api_key(request): authorization = request.META.get('HTTP_AUTHORIZATION') - print('_valid_api_key authorization: ' + authorization) + print('_valid_api_key authorization: ' + str(authorization)) + if not authorization: + return False m = re.match(r'^Basic (\w{64})', authorization) if m: received_key = m.group(1) @@ -63,7 +65,35 @@ def _valid_api_key(request): if db_key.key == received_key: return True return False - + +@require_http_methods(['GET']) +def token_by_nonce(request): + if not _valid_api_key(request): + return HttpResponseForbidden('must provide valid api key') + nonce = request.GET.get('nonce') + block = request.GET.get('block') + + # combine with other validation + block = int(block) + tokens = models.Token.objects.filter(nonce=nonce) + if tokens.count() > 0: + return JsonResponse(status=200, data={'access_token': token.access_token,'uid':token.user.id}) + else: + handler = redirect_handler.RedirectHandler() + lock = handler.block_nonce(nonce) + if not lock: #TODO clean this logic up + print('no lock returned from handler.block') + return HttpResponseNotFound('no token which meets required criteria') + with lock: + print('waiting for {} seconds'.format(block)) + lock.wait(block) + # see if it was actually satisfied, or just woken up for timeout + tokens = models.Token.objects.filter(nonce=nonce) + if tokens.count() == 0: + return HttpResponseNotFound('no token which meets required criteria') + else: + return JsonResponse(status=200, data={'access_token': token.access_token,'uid':token.user.id}) + @require_http_methods(['GET']) def token(request): @@ -71,11 +101,15 @@ def token(request): if not _valid_api_key(request): return HttpResponseForbidden('must provide valid api key') + print('GET params:') + for k,v in request.GET.items(): + print(k + ":" + v) uid = request.GET.get('uid') scope = request.GET.get('scope') provider = request.GET.get('provider') block = request.GET.get('block') - + nonce = request.GET.get('nonce') + # validate if block: if isint(block): @@ -87,20 +121,29 @@ def token(request): else: return HttpResponseBadRequest('if block param included, must be false or a positive integer') - if not uid: - return HttpResponseBadRequest('missing uid') + # nonce takes precedence over (scope,provider,uid) combination + if not nonce: + if not scope: + print('scope: ' + str(scope)) + return HttpResponseBadRequest('missing scope') + else: + scopes = scope.split(' ') + if len(scopes) == 0: + return HttpResponseBadRequest('no scopes provided') + + if not provider: + return HttpResponseBadRequest('missing provider') + + if not uid: + print('request received with no uid specified, will only generate url') + handler = redirect_handler.RedirectHandler() + url = handler.add(uid, scopes, provider) + return JsonResponse(status=401, data={'authorization_url': url}) - if scope: - scopes = scope.split(' ') - if len(scopes) == 0: - return HttpResponseBadRequest('no scopes provided') + if nonce: + tokens = models.Token.objects.filter(nonce=nonce) else: - return HttpResponseBadRequest('missing scope') - - if not provider: - return HttpResponseBadRequest('missing provider') - - tokens = _get_tokens(uid, scopes, provider) + tokens = _get_tokens(uid, scopes, provider) if tokens.count() == 0: print('no tokens met criteria') @@ -109,7 +152,11 @@ def token(request): if block: print('attempting block as required by client') #TODO block functionality - lock = handler.block(uid, scopes, provider) + if nonce: + lock = handler.block_nonce(nonce) + else: + lock = handler.block(uid, scopes, provider) + if not lock: #TODO clean this logic up print('no lock returned from handler.block') return HttpResponseNotFound('no token which meets required criteria') @@ -117,21 +164,25 @@ def token(request): print('waiting for {} seconds'.format(block)) lock.wait(block) # see if it was actually satisfied, or just woken up for timeout - tokens = _get_tokens(uid, scopes, provider) - if len(tokens) == 0: + if nonce: + models.Token.objects.filter(nonce=nonce) + else: + tokens = _get_tokens(uid, scopes, provider) + + if tokens.count() == 0: return HttpResponseNotFound('no token which meets required criteria') - # fall through else: # new authorization for this user and scopes url = handler.add(uid, scopes, provider) return JsonResponse(status=401, data={'authorization_url': url}) if tokens.count() > 1: - token = prune_duplicate_tokens(tokens) + token = tokens[0] # TODO + #token = prune_duplicate_tokens(tokens) else: token = tokens[0] - return JsonResponse(status=200, data={'access_token': token.access_token}) + return JsonResponse(status=200, data={'access_token': token.access_token,'uid':token.user.id}) def prune_duplicate_tokens(tokens): pass