Skip to content

Commit

Permalink
can initiate a generic authorization request, and block on the callba…
Browse files Browse the repository at this point in the history
…ck from a login using that unique nonce
  • Loading branch information
theferrit32 committed Apr 2, 2018
1 parent 6718a11 commit 13c224b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 42 deletions.
14 changes: 7 additions & 7 deletions src/microservice/token_service/crypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 5 additions & 4 deletions src/microservice/token_service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
65 changes: 55 additions & 10 deletions src/microservice/token_service/redirect_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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')
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions src/microservice/token_service/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]

Expand Down
93 changes: 72 additions & 21 deletions src/microservice/token_service/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -63,19 +65,51 @@ 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):
# api key authentication
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):
Expand All @@ -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')
Expand All @@ -109,29 +152,37 @@ 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')
with lock:
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
Expand Down

0 comments on commit 13c224b

Please sign in to comment.