Skip to content

Commit

Permalink
Support multiple servers in ejabberd auth.
Browse files Browse the repository at this point in the history
This is an update to the xidauth.py ejabberd authentication script, such
that is can support multiple server names and each with its own XID
application name and endpoint.  With this, it is possible to support
multiple domains on ejabberd, e.g. associated to XID on different
blockchains / networks.
  • Loading branch information
domob1812 committed Jun 13, 2022
1 parent 998f29d commit d02d7ea
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 79 deletions.
8 changes: 0 additions & 8 deletions ejabberd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,3 @@ Then, the following configuration options should be set in `ejabberd.yml`:

auth_method: external
extauth_program: "/etc/ejabberd/xidauth.py ...arguments..."

**Note: When Xid is running its JSON-RPC server over HTTP, it
[allows connections to the specified port
*from everywhere*](https://github.com/xaya/libxayagame/issues/41).
In particular, this
allows anyone to e.g. shut down the server. Thus, for Xid running on a
public server, it is important to block the RPC port from incoming
external connections through a suitable firewall (e.g. `iptables`)!**
6 changes: 2 additions & 4 deletions ejabberd/docker/xidauth.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#!/bin/sh -e

# Copyright (C) 2020 The Xaya developers
# Copyright (C) 2020-2022 The Xaya developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

# This is a simple wrapper script around xidauth.py, using some environment
# variables from the Docker context to set the arguments.

exec ${HOME}/bin/xidauth.py \
--xid_rpc_url "${XID_RPC_URL}" \
--application "${XID_APPLICATION}" \
--servername "${XMPP_DOMAIN}" \
--servers ${XIDAUTH_SERVERS} \
--logfile "${HOME}/logs/xidauth.log"
9 changes: 4 additions & 5 deletions ejabberd/tests/integration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# coding=utf8

# Copyright (C) 2019-2020 The Xaya developers
# Copyright (C) 2019-2022 The Xaya developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

Expand Down Expand Up @@ -123,7 +123,7 @@ def run (self):
# We need an instance of EjabberdXidAuth for decoding names. Create it
# and then only use it for that purpose. (This is not the instance
# that will be used in the real test.)
self.auth = EjabberdXidAuth ({}, "http://localhost", None)
self.auth = EjabberdXidAuth ({}, None)
self.auth.log = logging.getLogger ("xidauth")

# Define some test names (of various types) and set up signer addresses
Expand Down Expand Up @@ -173,9 +173,8 @@ def run (self):
srcdir = "."
binary = os.path.join (srcdir, "xidauth.py")
cmd = [binary]
cmd.append ("--xid_rpc_url=http://localhost:%s" % self.gamenode.port)
cmd.append ("--servername=%s" % self.server)
cmd.append ("--application=%s" % self.app)
rpcUrl = "http://localhost:%s" % self.gamenode.port
cmd.extend (["--servers", "%s,%s,%s" % (self.server, self.app, rpcUrl)])
cmd.append ("--logfile=%s" % os.path.join (self.basedir, "xidauth.log"))
try:
self.log.info ("Starting process: %s" % " ".join (cmd))
Expand Down
5 changes: 2 additions & 3 deletions ejabberd/tests/namecoding.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# coding=utf-8

# Copyright (C) 2019-2020 The Xaya developers
# Copyright (C) 2019-2022 The Xaya developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

Expand All @@ -15,8 +15,7 @@
class NameCodingTest (unittest.TestCase):

def setUp (self):
self.auth = EjabberdXidAuth ({}, "http://localhost",
logging.StreamHandler (sys.stderr))
self.auth = EjabberdXidAuth ({}, logging.StreamHandler (sys.stderr))

def testSimpleNames (self):
simpleNames = ["domob", "0", "foo42bar", "xxx"]
Expand Down
153 changes: 94 additions & 59 deletions ejabberd/xidauth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3

# Copyright (C) 2019-2020 The Xaya developers
# Copyright (C) 2019-2022 The Xaya developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

Expand All @@ -20,10 +20,81 @@
HEX_CHARS = string.digits + "abcdef"


class EjabberdXidAuth (object):
class EjabberdServer:
"""
The authentication service for a single XMPP server, which is
tied to a particular Xid application string and also
JSON-RPC endpoint (e.g. Xid on a particular network).
"""

def __init__ (self, application, xidRpcUrl):
self.application = application
self.xidRpc = jsonrpclib.ServerProxy (xidRpcUrl)
self.chain = self.xidRpc.getnullstate ()["chain"]

# The log is filled in when the service gets added to an instance
# of EjabberdXidAuth.
self.log = None

def unwrapGameState (self, data):
"""
Verifies that the game-state JSON in data represents an up-to-date
state (if not, an exception is raised). Then unwraps the contained
actual state from the "data" field and returns that.
"""

if data["state"] != "up-to-date":
self.log.critical ("xid is not up-to-date: %s" % data["state"])
raise RuntimeError ("xid is not up-to-date")

return data["data"]

def isUser (self, xayaName):
"""
Checks if the given username (already decoded to Xaya) is
a registered user of Xid on this service.
"""

data = self.xidRpc.getnamestate (name=xayaName)
state = self.unwrapGameState (data)

for entry in state["signers"]:
if len (entry["addresses"]) == 0:
continue

if not "application" in entry:
self.log.debug ("Found global signer key for %s" % xayaName)
return True

if entry["application"] == self.application:
self.log.debug ("Found signer key for %s and application %s"
% (xayaName, app))
return True

self.log.debug ("No valid signer keys for %s and application %s"
% (xayaName, self.application))
return False

def authenticate (self, xayaName, pwd):
"""
Checks if the given user (as decoded Xaya name) can be authenticated
with the given password on this Xid service.
"""

data = self.xidRpc.verifyauth (name=xayaName, application=self.application,
password=pwd)
state = self.unwrapGameState (data)

self.log.debug ("Authentication state from xid: %s" % state["state"])
return state["valid"]


class EjabberdXidAuth:
"""
The main class for an external authentication script for ejabberd that
uses xid to authenticate users on the XMPP server.
uses xid to authenticate users on the XMPP server. It has a mapping of
potentially multiple XMPP server names / domains to the associated
Xid authentication services, e.g. for different networks.
Note that XMPP has certain restrictions on the usernames; in particular,
names are treated in a case-insensitive way (other than Xaya). Thus,
Expand All @@ -32,13 +103,14 @@ class EjabberdXidAuth (object):
UTF-8.
"""

def __init__ (self, serverNames, xidRpcUrl, logHandler):
self.serverNames = serverNames
self.xidRpc = jsonrpclib.ServerProxy (xidRpcUrl)

def __init__ (self, services, logHandler):
if logHandler is not None:
self.setupLogging (logHandler)

self.serverNames = services
for s in self.serverNames.values ():
s.log = self.log

def setupLogging (self, handler):
logFmt = "%(asctime)s %(name)s (%(levelname)s): %(message)s"
handler.setFormatter (logging.Formatter (logFmt))
Expand Down Expand Up @@ -123,19 +195,6 @@ def decodeXmppName (self, name):
self.log.warning ("Simple name was hex-encoded: %s" % name)
return None

def unwrapGameState (self, data):
"""
Verifies that the game-state JSON in data represents an up-to-date
state (if not, an exception is raised). Then unwraps the contained
actual state from the "data" field and returns that.
"""

if data["state"] != "up-to-date":
self.log.critical ("xid is not up-to-date: %s" % data["state"])
raise RuntimeError ("xid is not up-to-date")

return data["data"]

def isUser (self, name, server):
"""
Checks whether the given XMPP name is a valid user for the given
Expand All @@ -146,31 +205,12 @@ def isUser (self, name, server):
if server not in self.serverNames:
self.log.warning ("Server %s is not configured for xidauth" % server)
return False
app = self.serverNames[server]

xayaName = self.decodeXmppName (name)
if xayaName is None:
return False

data = self.xidRpc.getnamestate (name=xayaName)
state = self.unwrapGameState (data)

for entry in state["signers"]:
if len (entry["addresses"]) == 0:
continue

if not "application" in entry:
self.log.debug ("Found global signer key for %s" % xayaName)
return True

if entry["application"] == app:
self.log.debug ("Found signer key for %s and application %s"
% (xayaName, app))
return True

self.log.debug ("No valid signer keys for %s and application %s"
% (xayaName, app))
return False
return self.serverNames[server].isUser (xayaName)

def authenticate (self, name, server, pwd):
"""
Expand All @@ -181,17 +221,12 @@ def authenticate (self, name, server, pwd):
if server not in self.serverNames:
self.log.warning ("Server %s is not configured for xidauth" % server)
return False
app = self.serverNames[server]

xayaName = self.decodeXmppName (name)
if xayaName is None:
return False

data = self.xidRpc.verifyauth (name=xayaName, application=app, password=pwd)
state = self.unwrapGameState (data)

self.log.debug ("Authentication state from xid: %s" % state["state"])
return state["valid"]
return self.serverNames[server].authenticate (xayaName, pwd)

def run (self, inp, outp):
"""
Expand All @@ -201,13 +236,10 @@ def run (self, inp, outp):
"""

servers = ""
for s, a in self.serverNames.items ():
servers += "\n %s: %s" % (s, a)
for n, s in self.serverNames.items ():
servers += "\n %s: %s (%s)" % (n, s.application, s.chain)
self.log.info ("Running xid authentication script for servers:" + servers)

data = self.xidRpc.getnamestate (name="xaya")
self.log.info ("Xid is %s at height %d" % (data["state"], data["height"]))

self.log.info ("Starting main loop...")
while True:
cmd = self.readCommand (inp)
Expand All @@ -230,16 +262,19 @@ def run (self, inp, outp):
if __name__ == "__main__":
desc = "ejabberd extauth script for authentication with xid"
parser = argparse.ArgumentParser (description=desc)
parser.add_argument ("--xid_rpc_url", required=True,
help="JSON-RPC URL for xid's RPC interface")
parser.add_argument ("--servername", required=True,
help="name of the XMPP server")
parser.add_argument ("--application", required=True,
help="application name for xid")
parser.add_argument ("--servers", required=True, nargs="+",
help="Server names to handle as server,application,rpcurl triplets")
parser.add_argument ("--logfile", default="/var/log/ejabberd/xidauth.log",
help="filename for writing logs to")
args = parser.parse_args ()

auth = EjabberdXidAuth ({args.servername: args.application}, args.xid_rpc_url,
logging.FileHandler (args.logfile))
services = {}
for s in args.servers:
parts = s.split (",")
if len (parts) != 3:
sys.exit ("Invalid server triplet: %s" % s)
[srv, app, rpcUrl] = parts
services[srv] = EjabberdServer (app, rpcUrl)

auth = EjabberdXidAuth (services, logging.FileHandler (args.logfile))
auth.run (sys.stdin.buffer, sys.stdout.buffer)

0 comments on commit d02d7ea

Please sign in to comment.