Skip to content

Commit

Permalink
Adding multiple authenticator support, HTTP Basic Authentication is d…
Browse files Browse the repository at this point in the history
…efault for SSL now, but Digest is allowed too. Digest is the only option when there is no SSL support
  • Loading branch information
proycon committed Sep 13, 2017
1 parent 00c4a05 commit 9e8e482
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 39 deletions.
32 changes: 23 additions & 9 deletions clam/clamservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -2385,21 +2385,23 @@ def __init__(self, mode = 'debug'):
error(msg)

if settings.OAUTH:
warning("*** Oauth Authentication is enabled. THIS IS NOT SECURE WITHOUT SSL! ***")
if not settings.ASSUMESSL: warning("*** Oauth Authentication is enabled. THIS IS NOT SECURE WITHOUT SSL! ***")
self.auth = clam.common.auth.OAuth2(settings.OAUTH_CLIENT_ID, settings.OAUTH_ENCRYPTIONSECRET, settings.OAUTH_AUTH_URL, getrooturl() + '/login/', settings.OAUTH_AUTH_FUNCTION, settings.OAUTH_USERNAME_FUNCTION, printdebug=printdebug,scope=settings.OAUTH_SCOPE)
elif settings.USERS:
digest_auth = clam.common.auth.HTTPDigestAuth(settings.SESSIONDIR,get_password=userdb_lookup_dict, realm=settings.REALM,debug=printdebug) #pylint: disable=redefined-variable-type
if settings.BASICAUTH:
self.auth = clam.common.auth.HTTPBasicAuth(get_password=userdb_lookup_dict, realm=settings.REALM,debug=printdebug) #pylint: disable=redefined-variable-type
warning("*** HTTP Basic Authentication is enabled. THIS IS NOT SECURE WITHOUT SSL! ***")
self.auth = clam.common.auth.MultiAuth(clam.common.auth.HTTPBasicAuth(get_password=userdb_lookup_dict, realm=settings.REALM,debug=printdebug), digest_auth) #pylint: disable=redefined-variable-type
if not settings.ASSUMESSL: warning("*** HTTP Basic Authentication is enabled. THIS IS NOT SECURE WITHOUT SSL! ***")
else:
self.auth = clam.common.auth.HTTPDigestAuth(settings.SESSIONDIR,get_password=userdb_lookup_dict, realm=settings.REALM,debug=printdebug) #pylint: disable=redefined-variable-type
self.auth = digest_auth
elif settings.USERS_MYSQL:
validate_users_mysql()
digest_auth = clam.common.auth.HTTPDigestAuth(settings.SESSIONDIR, get_password=userdb_lookup_mysql,realm=settings.REALM,debug=printdebug) #pylint: disable=redefined-variable-type
if settings.BASICAUTH:
self.auth = clam.common.auth.HTTPBasicAuth(get_password=userdb_lookup_mysql, realm=settings.REALM,debug=printdebug) #pylint: disable=redefined-variable-type
warning("*** HTTP Basic Authentication is enabled. THIS IS NOT SECURE WITHOUT SSL! ***")
self.auth = clam.common.auth.MultiAuth(clam.common.auth.HTTPBasicAuth(get_password=userdb_lookup_mysql, realm=settings.REALM,debug=printdebug), digest_auth) #pylint: disable=redefined-variable-type
if not settings.ASSUMESSL: warning("*** HTTP Basic Authentication is enabled. THIS IS NOT SECURE WITHOUT SSL! ***")
else:
self.auth = clam.common.auth.HTTPDigestAuth(settings.SESSIONDIR, get_password=userdb_lookup_mysql,realm=settings.REALM,debug=printdebug) #pylint: disable=redefined-variable-type
self.auth = digest_auth
elif settings.PREAUTHHEADER:
warning("*** Forwarded Authentication is enabled. THIS IS NOT SECURE WITHOUT A PROPERLY CONFIGURED AUTHENTICATION PROVIDER! ***")
self.auth = clam.common.auth.ForwardedAuth(settings.PREAUTHHEADER) #pylint: disable=redefined-variable-type
Expand Down Expand Up @@ -2519,8 +2521,6 @@ def set_defaults():
settings.USERS = None
if not 'ADMINS' in settingkeys:
settings.ADMINS = []
if not 'BASICAUTH' in settingkeys:
settings.BASICAUTH = False #default is HTTP Digest
if not 'LISTPROJECTS' in settingkeys:
settings.LISTPROJECTS = True
if not 'ALLOWSHARE' in settingkeys: #TODO: all these are not implemented yet
Expand Down Expand Up @@ -2658,6 +2658,13 @@ def set_defaults():
if not 'ALLOW_ORIGIN' in settingkeys:
settings.ALLOW_ORIGIN = '*'

if not 'ASSUMESSL' in settingkeys:
settings.ASSUMESSL = settings.PORT == 443

if not 'BASICAUTH' in settingkeys and (settings.USERS or settings.USERS_MYSQL) and settings.ASSUMESSL:
settings.BASICAUTH = True #Allowing HTTP Basic ALONGSIDE HTTP Digest
elif not 'BASICAUTH' in settingkeys:
settings.BASICAUTH = False #default is HTTP Digest

def test_dirs():
if not os.path.isdir(settings.ROOT):
Expand Down Expand Up @@ -2727,13 +2734,15 @@ def main():
settingsmodule = None
PORT = HOST = FORCEURL = None
PYTHONPATH = None
ASSUMESSL = False

parser = argparse.ArgumentParser(description="Start a CLAM webservice; turns command-line tools into RESTful webservice, including a web-interface for human end-users.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-d','--debug',help="Enable debug mode", action='store_true', required=False)
parser.add_argument('-H','--hostname', type=str,help="The hostname used to access the webservice", action='store',required=False)
parser.add_argument('-p','--port', type=int,help="The port number for the webservice", action='store',required=False)
parser.add_argument('-u','--forceurl', type=str,help="The full URL to access the webservice", action='store',required=False)
parser.add_argument('-P','--pythonpath', type=str,help="Sets the $PYTHONPATH", action='store',required=False)
parser.add_argument('-b','--basicauth', help="Default to HTTP Basic Authentication on the development server (do not expose to the world without SSL)", action='store_true',required=False)
parser.add_argument('-v','--version',help="Version", action='version',version="CLAM version " + str(VERSION))
parser.add_argument('settingsmodule', type=str, help='The webservice service configuration to be imported. This is a Python module path rather than a file path (for instance: clam.config.textstats), the configuration must be importable by Python. Add the path where it is located using --pythonpath if it can not be found.')
args = parser.parse_args()
Expand All @@ -2751,6 +2760,8 @@ def main():
FORCEURL = args.forceurl
if 'pythonpath' in args:
PYTHONPATH = args.pythonpath
if 'basicauth' in args:
ASSUMESSL = True

settingsmodule = args.settingsmodule

Expand Down Expand Up @@ -2783,6 +2794,9 @@ def main():
settings.FORCEURL = FORCEURL
if PORT:
settings.PORT = PORT
if ASSUMESSL:
settings.ASSUMESSL = ASSUMESSL
settings.BASICAUTH = True

if settings.URLPREFIX:
settings.STANDALONEURLPREFIX = settings.URLPREFIX
Expand Down
74 changes: 54 additions & 20 deletions clam/common/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import flask

import clam.common.oauth
from clam.common.digestauth import pwhash

try:
from requests_oauthlib import OAuth2Session
Expand Down Expand Up @@ -73,8 +74,10 @@ def decorated(*args, **kwargs):
def require_login(self, f):
@wraps(f)
def decorated(*args, **kwargs):
self.printdebug("Handling HTTP Authentication (Basic/Digest)")
self.printdebug("Handling HTTP " + self.scheme + " Authentication")
auth = flask.request.authorization


# We need to ignore authentication headers for OPTIONS to avoid
# unwanted interactions with CORS.
# Chrome and Firefox issue a preflight OPTIONS request to check
Expand Down Expand Up @@ -109,43 +112,32 @@ def username(self, **kwargs):


class HTTPBasicAuth(HTTPAuth):

def __init__(self, **kwargs):
super(HTTPBasicAuth, self).__init__(**kwargs)
if 'realm' in kwargs:
self.realm = kwargs['realm']
else:
self.realm = "Default realm"
self.hash_password(None)
self.verify_password(None)

def hash_password(self, f):
self.hash_password_callback = f
return f

def verify_password(self, f):
self.verify_password_callback = f
return f
self.printdebug("Initialising Basic Authentication with realm " + self.realm)
self.scheme = "Basic"

def authenticate_header(self):
return 'Basic realm="{0}"'.format(self.realm)

def authenticate(self, auth, stored_password):
client_password = auth.password
if self.verify_password_callback:
return self.verify_password_callback(auth.username, client_password)
if self.hash_password_callback:
try:
client_password = self.hash_password_callback(client_password)
except TypeError:
client_password = self.hash_password_callback(auth.username, client_password)
return client_password == stored_password
return pwhash(auth.username, self.realm, auth.password) == stored_password


class HTTPDigestAuth(HTTPAuth):

def __init__(self, noncedir, **kwargs):
super(HTTPDigestAuth, self).__init__(**kwargs)
if 'realm' in kwargs:
self.realm = kwargs['realm']
else:
self.realm = "Default realm"
self.scheme = "Digest"


if 'nonceexpiration' in kwargs:
Expand Down Expand Up @@ -286,6 +278,48 @@ def username(self, **kwargs):
return flask.request.headers[h]
raise KeyError

class MultiAuth(object):
def __init__(self, main_auth, *args,printdebug=False):
self.main_auth = main_auth
self.additional_auth = args
if printdebug:
self.printdebug = printdebug
else:
self.printdebug = lambda x: print(x,file=sys.stderr)
self.printdebug("Initialized multiple authenticators: " + repr(self.main_auth) + "," + repr(self.additional_auth))

def require_login(self, f):
@wraps(f)
def decorated(*args, **kwargs):
self.printdebug("Handling Multiple Authenticators")
selected_auth = None
if 'Authorization' in flask.request.headers:
try:
scheme, creds = flask.request.headers['Authorization'].split( None, 1)
self.printdebug("Requested scheme = " + scheme)
except ValueError:
# malformed Authorization header
self.printdebug("Malformed authorization header")
pass
else:
for auth in self.additional_auth:
if auth.scheme == scheme:
selected_auth = auth
break
else:
self.printdebug("No authorization header passed")
res = flask.make_response("Authorization required")
res.status_code = 401
res.headers.add('WWW-Authenticate', self.main_auth.authenticate_header())
for auth in self.additional_auth:
res.headers.add('WWW-Authenticate', auth.authenticate_header())
return res

if selected_auth is None:
selected_auth = self.main_auth
return selected_auth.require_login(f)(*args, **kwargs)
return decorated

class OAuth2(HTTPAuth):
def __init__(self, client_id, encryptionsecret, auth_url, redirect_url, auth_function, username_function, printdebug=None, scope=None): #pylint: disable=super-init-not-called
def default_auth_error():
Expand Down
28 changes: 18 additions & 10 deletions docs/clam_manual.tex
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ \chapter{Introduction}
\item \textbf{Extensible} -- \emph{Due to a modular setup, CLAM is quite extensible}
\item \textbf{Client and Data API} -- \emph{A rich Python API for writing CLAM Clients and system wrappers}
\item \textbf{Authentication} -- \emph{A user-based authentication mechanism
through HTTP Digest is provided. Morever, OAuth2 is also supported for delegating
through HTTP Digest and/or HTTP Basic is provided. Morever, OAuth2 is also supported for delegating
authentication}
\item \textbf{Metadata and provenance data} -- \emph{Extensive support for metadata and provenance data is offered}
\item \textbf{Automatic converters} -- \emph{Automatic converters enable conversion from an auxiliary format into the desired input format, and conversion from the produced output format into an auxiliary output format}
Expand Down Expand Up @@ -469,7 +469,7 @@ \subsection{Deploying CLAM with Apache 2 through mod\_proxy\_uwsgi)}
installation and versions and is subject to change on an upgrade.
\item It is always recommended to add some form of authentication or more restrictive
access. You can either let CLAM handle authentication (\emph{HTTP Digest
access. You can either let CLAM handle authentication (\emph{HTTP Basic or Digest
Authentication} or \emph{OAuth2}), or you can let Apache itself handle
authentication and not use CLAM's authentication mechanism.
\item Restart Apache
Expand Down Expand Up @@ -527,7 +527,7 @@ \subsection{Deploying CLAM with Apache 2 through mod\_wsgi)}
\item It is always recommended to add some form of authentication or more restrictive
access. You can either let CLAM handle authentication (\emph{HTTP Digest
access. You can either let CLAM handle authentication (\emph{HTTP Basic or Digest
Authentication} or \emph{OAuth2}), in which case you need to set \texttt{WSGIPassAuthorization
On}, as by default it is disabled, or you can let Apache itself handle
authentication and not use CLAM's authentication mechanism.
Expand All @@ -536,7 +536,7 @@ \subsection{Deploying CLAM with Apache 2 through mod\_wsgi)}
Note that we run WSGI in Daemon mode using the \texttt{WSGIDaemonProcess} and
\texttt{WSGIProcessGroup} directives, as opposed to embedded mode. This is the
recommended way of running CLAM, and is even mandatory when using HTTP Digest
recommended way of running CLAM, and is even mandatory when using HTTP Basic/Digest
Authentication. Whenever any code changes are made, simply
\texttt{touch} the WSGI file (updating its modification time), and the changes
will be immediately available. Embedded mode would require an apache restart
Expand Down Expand Up @@ -842,12 +842,20 @@ \subsection{Server Administration}
\subsection{User Authentication}
Being a RESTful webservice, user authentication proceeds over HTTP itself. CLAM
implements HTTP Digest Authentication \cite{HTTPAUTH} and OAuth2 \cite{OAUTH2}. HTTP Digest
Authentication, contrary to HTTP Basic Authentication, computes a hash of the
username and password client-side and transmits that hash, rather than a
plaintext password. User passwords are therefore only available to CLAM in
hashed form. User authentication is not mandatory, but for any world-accessible
Being a RESTful webservice, user authentication proceeds over HTTP itself. CLAM implements HTTP Basic Authentication,
HTTP Digest Authentication \cite{HTTPAUTH} and OAuth2 \cite{OAUTH2}. HTTP Digest Authentication, contrary to HTTP Basic
Authentication, computes a hash of the username and password client-side and transmits that hash, rather than a
plaintext password. User passwords are therefore only available to CLAM in hashed form and are not transmitted
unencrypted, even over a HTTP connection. HTTP Basic Authentication, conversely, should only be use over SSL (i.e.
HTTPS), and CLAM will by default disallow it if it thinks it's not running on an SSL connection.
CLAM itself does not provide SSL on the built-in development server as this is delegated to your production webserver
(Apache or Nginx) instead. If you are using SSL but CLAM does not detect it, you can set \texttt{ASSUMESSL = True}. In
this case HTTP Basic Authentication will be the default authentication mechanism since CLAM 2.2, but HTTP Digest
Authentication is accepted too. If you're not on an SSL connection, CLAM will default to HTTP Digest Authentication
only and disallow HTTP Basic Authentication.
User authentication is not mandatory, but for any world-accessible
environment it is most strongly recommended, for obvious security reasons.
A list of user accounts and passwords can be defined in \texttt{USERS} in the
Expand Down

0 comments on commit 9e8e482

Please sign in to comment.