From f6a17b5f32fdb21e5ad47a18df0d79d2c72a290a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 15:58:20 -0400 Subject: [PATCH] Have validate serve as session info request This should facilitate login. Further, provide a quick persistence for the credential test backend --- confluent_server/confluent/auth.py | 2 +- confluent_server/confluent/httpapi.py | 43 ++++++++++++++++---------- confluent_server/confluent/webauthn.py | 36 ++++++++++++++++++--- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 02828f36..6ded2e70 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,7 +162,7 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) - if element in ('/sessions/current/webauthn/registered_credentials', '/sessions/current/webauthn/validate'): + if element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/'): return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index ece18980..ec80a095 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -269,7 +269,7 @@ def _csrf_valid(env, session): env['HTTP_CONFLUENTAUTHTOKEN'] == session['csrftoken']) -def _authorize_request(env, operation): +def _authorize_request(env, operation, reqbody): """Grant/Deny access based on data from wsgi env """ @@ -282,8 +282,7 @@ def _authorize_request(env, operation): if element.startswith('/sessions/current/'): if (element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/')): - username = element.rsplit('/')[-1] - element = element.replace('/' + username, '') + name = element.rsplit('/')[-1] authdata = auth.authorize(name, element=element, operation=operation) else: element = None @@ -335,18 +334,13 @@ def _authorize_request(env, operation): return {'code': 403} elif not authdata: return {'code': 401} - sessid = util.randomstring(32) - while sessid in httpsessions: - sessid = util.randomstring(32) - httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90, - 'skipuserobject': authdata[4], - 'inflight': set([])} - if 'HTTP_CONFLUENTAUTHTOKEN' in env: - httpsessions[sessid]['csrftoken'] = util.randomstring(32) - cookie['confluentsessionid'] = util.stringify(sessid) - cookie['confluentsessionid']['secure'] = 1 - cookie['confluentsessionid']['httponly'] = 1 - cookie['confluentsessionid']['path'] = '/' + sessid = _establish_http_session(env, authdata, name, cookie) + if authdata and element and element.startswith('/sessions/current/webauthn/validate/'): + if webauthn: + for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None): + if rsp['verified']: + sessid = _establish_http_session(env, authdata, name, cookie) + break skiplog = _should_skip_authlog(env) if authdata: auditmsg = { @@ -376,6 +370,21 @@ def _authorize_request(env, operation): else: return {'code': 403} +def _establish_http_session(env, authdata, name, cookie): + sessid = util.randomstring(32) + while sessid in httpsessions: + sessid = util.randomstring(32) + httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90, + 'skipuserobject': authdata[4], + 'inflight': set([])} + if 'HTTP_CONFLUENTAUTHTOKEN' in env: + httpsessions[sessid]['csrftoken'] = util.randomstring(32) + cookie['confluentsessionid'] = util.stringify(sessid) + cookie['confluentsessionid']['secure'] = 1 + cookie['confluentsessionid']['httponly'] = 1 + cookie['confluentsessionid']['path'] = '/' + return sessid + def _pick_mimetype(env): """Detect the http indicated mime to send back. @@ -616,7 +625,7 @@ def resourcehandler_backend(env, start_response): if operation != 'retrieve' and 'restexplorerop' in querydict: operation = querydict['restexplorerop'] del querydict['restexplorerop'] - authorized = _authorize_request(env, operation) + authorized = _authorize_request(env, operation, reqbody) if 'logout' in authorized: start_response('200 Successful logout', headers) yield('{"result": "200 - Successful logout"}') @@ -848,7 +857,7 @@ def resourcehandler_backend(env, start_response): start_response('501 Not Implemented', headers) yield '' return - for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody): + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody, authorized): yield rsp return resource = '.' + url[url.rindex('/'):] diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 731b33e5..a11984c7 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,34 +1,51 @@ import base64 +import confluent.tlvdata as tlvdata import confluent.util as util import json import pywarp import pywarp.backends +import pywarp.credentials creds = {} challenges = {} class TestBackend(pywarp.backends.CredentialStorageBackend): def __init__(self): - pass + global creds + try: + with open('/tmp/mycreds.json', 'r') as ji: + creds = json.load(ji) + except Exception: + pass def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - return creds[email] + cred = creds[email] + cid = base64.b64decode(cred['cid']) + cpk = base64.b64decode(cred['cpk']) + return pywarp.credentials.Credential(credential_id=cid, credential_public_key=cpk) def save_credential_for_user(self, email, credential): if not isinstance(email, str): email = email.decode('utf8') + credential = {'cid': base64.b64encode(credential.id).decode('utf8'), 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} creds[email] = credential + with open('/tmp/mycreds.json', 'w') as jo: + json.dump(creds, jo) def save_challenge_for_user(self, email, challenge, type): + if not isinstance(email, str): + email = email.decode('utf8') challenges[email] = challenge def get_challenge_for_user(self, email, type): + if not isinstance(email, str): + email = email.decode('utf8') return challenges[email] -def handle_api_request(url, env, start_response, username, cfm, headers, reqbody): +def handle_api_request(url, env, start_response, username, cfm, headers, reqbody, authorized): if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') @@ -72,8 +89,17 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) req['email'] = username rsp = rp.verify(**req) - start_response('200 OK') - yield json.dumps(rsp) + if start_response: + start_response('200 OK', headers) + sessinfo = {'username': username} + if 'authtoken' in authorized: + sessinfo['authtoken'] = authorized['authtoken'] + if 'sessionid' in authorized: + sessinfo['sessionid'] = authorized['sessionid'] + tlvdata.unicode_dictvalues(sessinfo) + yield json.dumps(sessinfo) + else: + yield rsp elif url == '/register_credential': rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) req = json.loads(reqbody)