Subject: [PATCH] Add support for cookie based auth after initial password auth

From: Josh Lukens <[email-masked]>
Date: Wed, 5 Mar 2014 23:39:55 -0500

Package: x2gobroker
Version: 0.0.2.4
Severity: wishlist

---
etc/x2gobroker.conf | 30 +++++++--
x2gobroker/brokers/base_broker.py | 133 +++++++++++++++++++++-----------------
x2gobroker/defaults.py | 7 +-
x2gobroker/web/json.py | 9 +--
x2gobroker/web/plain.py | 10 ++-
x2gobroker/web/uccs.py | 4 +-
6 files changed, 112 insertions(+), 81 deletions(-)

diff --git a/etc/x2gobroker.conf b/etc/x2gobroker.conf
index 19ea93b..b8b8974 100644
--- a/etc/x2gobroker.conf
+++ b/etc/x2gobroker.conf
@@ -24,20 +24,38 @@

[global]

-# Allow unauthenticated connections? Then set check-credentials to false.
-#check-credentials = true
+# Allow unauthenticated connections? Then set both require-password and require-cookie to false.
+
+# Veriy username/password combination sent by client
+#require-password = true

# To secure server-client communication the client can start the communication
# with a pre-set, agreed on authentication ID. Set the below value to true
# to make the X2Go Session Broker require this feature
-#require-cookie-auth = false ### NOT-IN-USE-YET
+#require-cookie = false

# X2Go supports two different cookie authentication modes (static and dynamic).
-#use-static-cookie = true ### NOT-IN-USE-YET
+# Dynamic cookies send new cookie to client on every request. This could possibly
+# cause issues if a client ever tries multiple requests at the same time.
+#use-static-cookie = true
+
+# Once a client is authenticated their password is not revalidated until this
+# many seconds have elapsed from their initial authentication.
+#auth-timeout = 36000
+
+# Client cookies (both static and dynamic) must be stored as local files.
+# This is the directory where those files will be stored. Please make sure
+# the permissions are set to allow the x2go broker process to write to this directory
+#cookie-directory = '/var/log/x2gobroker/cookies'

# Every server-client communication (between X2Go Client and broker) has to be
-# accompanied by this initial authentication cookie.
-#my-cookie = <aaaavveeeerrrrryyyyylooonnnnggggssttrrriiinnnggg> ### NOT-IN-USE-YET
+# accompanied by this initial authentication cookie if require-cookie is set above.
+# This should be in the format of a UUID.
+#my-cookie = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+
+# By default the broker will pin user sessions to the IP address from which they
+# origionally authenticate. If you would like to skip that check set this to false.
+#verify-ip = true

# X2Go Session Broker knows about two output formats: a text/plain based output
# and a text/json based output that is compatible with UCCS. The different outputs
diff --git a/x2gobroker/brokers/base_broker.py b/x2gobroker/brokers/base_broker.py
index 7fb3172..2f7c7ad 100644
--- a/x2gobroker/brokers/base_broker.py
+++ b/x2gobroker/brokers/base_broker.py
@@ -726,7 +726,7 @@ class X2GoBroker(object):
else:
return []

- def check_access(self, username='', password='', cookie=None, cookie_only=False):
+ def check_access(self, username='', password='', ip='', cookie=None):
"""\
Check if a given user with a given password may gain access to the
X2Go session broker.
@@ -735,80 +735,95 @@ class X2GoBroker(object):
@type username: C{unicode}
@param password: a password that authenticates the user against the X2Go session broker
@type password: C{unicode}
+ @param ip: the ip address of the client
+ @type ip: C{unicode}
@param cookie: an extra (static or dynamic) authentication token
@type cookie: C{unicode}
- @param cookie_only: do only check the auth_cookie, not username/password
- @type cookie_only: C{bool}

@return: returns C{True} if the authentication has been successful
- @rtype: C{bool}
+ @rtype: C{bool},C{unicode}

"""
### FOR INTRANET LOAD BALANCER WE MAY JUST ALLOW ACCESS TO EVERYONE
### This is handled through the config file, normally /etc/x2go/x2gobroker.conf

- if not self.config.get_value('global', 'check-credentials'):
+ if not self.config.get_value('global', 'require-password') and not self.config.get_value('global', 'require-cookie'):
logger_broker.debug('base_broker.X2GoBroker.check_access(): access is granted without checking credentials, prevent this in {configfile}'.format(configfile=self.config_file))
- return True
+ return True,0
elif username == 'check-credentials' and password == 'FALSE':
# this catches a validation check from the UCCS web frontend...
- return False
+ return False,0

### IMPLEMENT YOUR AUTHENTICATION LOGIC IN THE self._do_authenticate(**kwargs) METHOD
### when inheriting from the base.X2GoBroker class.
-
- access = False
- if cookie_only is False:
- access = self._do_authenticate(username=username, password=password)
- logger_broker.debug('base_broker.X2GoBroker.check_access(): result of authentication check is: {access}'.format(access=access))
- else:
- access = True
-
- ### HANDLING OF DYNAMIC AUTHENTICATION ID HASHES
-
- # using cookie authentication as extra security?
- if self.config.get_value('global', 'require-cookie-auth'):
-
- if type(cookie) is types.StringType:
- cookie = unicode(cookie)
-
- if self.config.get_value('global', 'use-static-cookie'):
-
- # evaluate access based on static authentication ID feature
- access = access and ( cookie == self.config.get_value('global', 'my-cookie') )
-
- else:
-
- # evaluate access based on dynamic authentication ID feature
- if self._dynamic_cookie_map.has_key(username):
- access = access and ( cookie == self._dynamic_cookie_map[username] )
- if access:
- self._dynamic_cookie_map[username] = uuid.uuid5(namespace=cookie, name=username)
-
+ if type(cookie) is types.StringType:
+ cookie = unicode(cookie)
+
+ if (((cookie == None) or (cookie == "")) and self.config.get_value('global', 'require-cookie')):
+ #cookie required but we did not get one - catch wrong cookie case later
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): cookie required but none given.')
+ return False, 0
+ #check if cookie sent was our preset cookie from config file
+ next_cookie = self.config.get_value('global', 'my-cookie')
+ access = (cookie == next_cookie )
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): checking if our configured cookie was submitted: {access}'.format(access=access))
+ #the require cookie but not password case falls through to returning value of access
+
+ if self.config.get_value('global', 'require-password'):
+ #using files to store persistant cookie information because global variables do not work across threads in WSGI
+ import os.path
+ cookie_directory=self.config.get_value('global', 'cookie-directory')
+ if(not os.path.isdir(cookie_directory)):
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): cookie-directory {cookie_directory} does not exist trying to craete it'.format(cookie_directory=cookie_directory))
+ try:
+ os.makedirs(cookie_directory);
+ except:
+ logger_broker.warning('base_broker.X2GoBroker.check_access(): could not create cookie-directory {cookie_directory} failing to authenticate'.format(cookie_directory=cookie_directory))
+ return False, 0
+ if access or cookie == None or cookie == "":
+ # this should be the first time we have seen this user or they are using old client so verify their passwrd
+ access = self._do_authenticate(username=username, password=password)
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): checking for valid password: {access}'.format(access=access))
+
+ if access:
+ #create new cookie for this user
+ #each user gets one or more tuples of IP, time stored as username_UUID files so they can connect from multiple sessions
+ next_cookie = str(uuid.uuid4())
+ fh = open(cookie_directory+"/"+username+"_"+next_cookie,"w")
+ fh.write('{ip} {time}'.format(ip=ip, time=time.time()))
+ fh.close()
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): Giving new cookie: {cookie} to user {username} at ip {ip}'.format(cookie=next_cookie,username=username,ip=ip))
+ else:
+ # there is a cookie but its not ours so its either wrong or subsequent password auth
+ if os.path.isfile(cookie_directory+"/"+username+"_"+cookie):
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): found valid auth key for user cookie: {usercookie}'.format(usercookie=username+"_"+cookie))
+ fh=open(cookie_directory+"/"+username+"_"+cookie,"r")
+ origip,origtime= fh.read().split()
+ fh.close()
+ os.unlink(cookie_directory+"/"+username+"_"+cookie)
+ #found cookie - make sure IP and time are good
+ if self.config.get_value('global', 'verify-ip') and (ip != origip):
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): IPs differ (new: {ip} old: {origip}) - rejecting user'.format(ip=ip,origip=origip))
+ return False, 0
+ if (time.time() - float(origtime)) > self.config.get_value('global', 'auth-timeout'):
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): Too much time elapsed since origional auth - rejecting user')
+ return False, 0
+ if self.config.get_value('global', 'use-static-cookie'):
+ #if using static cookies keep same cookie as user presented
+ next_cookie = cookie
+ else:
+ #otherwise give them new random cookie
+ next_cookie = str(uuid.uuid4())
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): Giving cookie: {cookie} to ip {ip}'.format(cookie=next_cookie, ip=ip))
+ fh = open(cookie_directory+"/"+username+"_"+next_cookie,"w")
+ fh.write('{ip} {time}'.format(ip=ip, time=origtime))
+ fh.close()
+ access = True
else:
- access = access and ( cookie == self.config.get_value('global', 'my-cookie') )
- if access:
- # generate a first uuid, initialize the dynamic authencation ID security feature
- self._dynamic_cookie_map[username] = uuid.uuid4()
-
- return access
-
- def get_next_cookie(self, username):
- """\
- Get the next expected authentication cookie for the given user name.
-
- @param username: query next authentication cookie for this user
- @type username: C{unicode}
-
- @return: returns next authentication cookie for the given username, None if no cookie has been generated, yet
- @rtype: C{unicode} or C{None}
-
- """
- try:
- return self._dynamic_cookie_map[username]
- except KeyError:
- return None
-
+ # client sent us an unknown cookie so failing auth
+ logger_broker.debug('base_broker.X2GoBroker.check_access(): User {username} from {ip} presented cookie {cookie} which is not recognized - rejecting user'.format(username=username, cookie=cookie, ip=ip))
+ return False, 0
+ return access, next_cookie

def get_remote_agent(self, profile_id, exclude_agents=[], ):
"""\
diff --git a/x2gobroker/defaults.py b/x2gobroker/defaults.py
index e65fd31..9027ed0 100644
--- a/x2gobroker/defaults.py
+++ b/x2gobroker/defaults.py
@@ -180,9 +180,12 @@ X2GOBROKER_HOME = os.path.normpath(os.path.expanduser('~{broker_uid}'.format(bro
# defaults for X2Go Sessino Broker configuration file
X2GOBROKER_CONFIG_DEFAULTS = {
'global': {
- u'check-credentials': True,
- u'require-cookie-auth': False,
+ u'require-password': True,
+ u'require-cookie': False,
u'use-static-cookie': True,
+ u'auth-timeout': 36000,
+ u'cookie-directory': '/var/log/x2gobroker/cookies',
+ u'verify-ip': True,
u'my-cookie': uuid.uuid4(),
u'enable-plain-output': True,
u'enable-json-output': True,
diff --git a/x2gobroker/web/json.py b/x2gobroker/web/json.py
index b217050..1f10b31 100644
--- a/x2gobroker/web/json.py
+++ b/x2gobroker/web/json.py
@@ -119,17 +119,14 @@ class X2GoBrokerWeb(_RequestHandler):
output = ''

logger_broker.debug ('username: {username}, password: {password}, task: {task}, profile_id: {profile_id}, cookie: {cookie}'.format(username=username, password='XXXXX', task=task, profile_id=profile_id, cookie=cookie))
- if broker_backend.check_access(username=username, password=password, cookie=cookie):
+ access, next_cookie = broker_backend.check_access(username=username, password=password, ip=ip, cookie=cookie)
+ if access:

###
### CONFIRM SUCCESSFUL AUTHENTICATION FIRST
###

- if global_config['require-cookie-auth'] and not global_config['use-static-cookie']:
-
- ### FIXME: make up a nice protocol for this, disabled for now
- #output += "AUTHID: {authid}<br />".format(authid=broker_backend.get_next_authid(username=data.user))
- pass
+ ### FIXME: find good way to pass next cookie to client - stored in next_cookie

###
### X2GO BROKER TASKS
diff --git a/x2gobroker/web/plain.py b/x2gobroker/web/plain.py
index 9d58742..254c9d2 100644
--- a/x2gobroker/web/plain.py
+++ b/x2gobroker/web/plain.py
@@ -115,17 +115,15 @@ class X2GoBrokerWeb(_RequestHandler):
output = ''

logger_broker.debug ('username: {username}, password: {password}, task: {task}, profile_id: {profile_id}, cookie: {cookie}'.format(username=username, password='XXXXX', task=task, profile_id=profile_id, cookie=cookie))
- if broker_backend.check_access(username=username, password=password, cookie=cookie):
+ access, next_cookie = broker_backend.check_access(username=username, password=password, ip=ip, cookie=cookie)
+ if access:

###
### CONFIRM SUCCESSFUL AUTHENTICATION FIRST
###

- if global_config['require-cookie-auth'] and not global_config['use-static-cookie']:
-
- ### FIXME: make up a nice protocol for this, disabled for now
- #output += "AUTHID: {authid}<br />".format(authid=broker_backend.get_next_authid(username=data.user))
- pass
+ if next_cookie != 0:
+ output += "AUTHID:{authid}\n".format(authid=next_cookie)

output += "Access granted\n"
###
diff --git a/x2gobroker/web/uccs.py b/x2gobroker/web/uccs.py
index 917704f..87dc64a 100644
--- a/x2gobroker/web/uccs.py
+++ b/x2gobroker/web/uccs.py
@@ -42,11 +42,11 @@ def credentials_validate(username, password):
# from x2gobroker.conf are available here...
broker = x2gobroker.brokers.base_broker.X2GoBroker()
broker.enable()
- access = broker.check_access(username=username, password=password)
+ access, next_cookie = broker.check_access(username=username, password=password)
# UCCS only allows email addresses for remote login 
if not access and "@" in username:
username = username.split('@')[0]
- access = broker.check_access(username=username, password=password)
+ access, next_cookie = broker.check_access(username=username, password=password)
if username == 'check-credentials' and password == 'FALSE':
username = 'anonymous'
return username, access
-- 
1.8.3.4 (Apple Git-47)