diff --git a/README.md b/README.md
index 78c6fca0eb7792bd7d7d92b85d1822c0a1c2f41a..8bca22a345a07c3364b944a5d35167e632fc56db 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Please note that we refer to Debian packages here and **not** pip packages.
 - python3-fido2 (version 0.5.0 or 0.9.1, optional)
 - python3-oauthlib
 - python3-flask-babel
+- python3-argon2
 - python3-mysqldb or python3-pymysql for MySQL/MariaDB support
 
 Some of the dependencies (especially fido2) changed their API in recent versions, so make sure to install the versions from Debian Buster or Bullseye.
diff --git a/debian/control b/debian/control
index 072db47ddbbc16afa0d8b6e34537c8754e6e3855..f9fef23b94d2f0720ab7773a2c0aae6ab26c3cdd 100644
--- a/debian/control
+++ b/debian/control
@@ -25,6 +25,7 @@ Depends:
  python3-fido2,
  python3-oauthlib,
  python3-flask-babel,
+ python3-argon2,
  uwsgi,
  uwsgi-plugin-python3,
 Recommends:
diff --git a/setup.py b/setup.py
index 5f45af544072bf886d049bfe1a9528ad96455adc..ad1822a8314c01583dede68d10682cc3da49c05f 100644
--- a/setup.py
+++ b/setup.py
@@ -40,6 +40,7 @@ setup(
 		'Flask-Migrate==2.1.1',
 		'Flask-Babel==0.11.2',
 		'alembic==1.0.0',
+		'argon2-cffi==18.3.0',
 
 		# The main dependencies on their own lead to version collisions and pip is
 		# not very good at resolving them, so we pin the versions from Debian Buster
diff --git a/tests/test_password_hash.py b/tests/test_password_hash.py
index ed17b895457b450ef643e39b1d5d255c12c9061d..834b67ef76868427a180099265766ea59e002ffa 100644
--- a/tests/test_password_hash.py
+++ b/tests/test_password_hash.py
@@ -130,6 +130,31 @@ class TestCryptPasswordHash(unittest.TestCase):
 		self.assertTrue(obj.verify('password'))
 		self.assertFalse(obj.verify('notpassword'))
 
+class TestArgon2PasswordHash(unittest.TestCase):
+	def test_verify(self):
+		obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=2,p=8$Jc8LpCgPLjwlN/7efHLvwQ$ZqSg3CFb2/hBb3X8hOq4aw')
+		self.assertTrue(obj.verify('password'))
+		self.assertFalse(obj.verify('notpassword'))
+		obj = Argon2PasswordHash('{argon2}$invalid$')
+		self.assertFalse(obj.verify('password'))
+
+	def test_from_password(self):
+		obj = Argon2PasswordHash.from_password('password')
+		self.assertIsNotNone(obj.value)
+		self.assertTrue(obj.value.startswith('{argon2}'))
+		self.assertTrue(obj.verify('password'))
+		self.assertFalse(obj.verify('notpassword'))
+
+	def test_needs_rehash(self):
+		obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=2,p=8$Jc8LpCgPLjwlN/7efHLvwQ$ZqSg3CFb2/hBb3X8hOq4aw')
+		self.assertFalse(obj.needs_rehash)
+		obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=2,p=8$Jc8LpCgPLjwlN/7efHLvwQ$ZqSg3CFb2/hBb3X8hOq4aw', target_cls=PlaintextPasswordHash)
+		self.assertTrue(obj.needs_rehash)
+		obj = Argon2PasswordHash('{argon2}$argon2d$v=19$m=102400,t=2,p=8$kshPgLU1+h72l/Z8QWh8Ig$tYerKCe/5I2BCPKu8hCl2w')
+		self.assertTrue(obj.needs_rehash)
+		obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=1,p=8$aa6i4vg/szKX5xHVGFaAeQ$v6j0ltuVqQaZlmuepaVJ1A')
+		self.assertTrue(obj.needs_rehash)
+
 class TestInvalidPasswordHash(unittest.TestCase):
 	def test(self):
 		obj = InvalidPasswordHash('test')
diff --git a/uffd/password_hash.py b/uffd/password_hash.py
index 4862cc36562612604aaf35d5f9ff7d61309e7faa..bd941ea1a76befb7575e6435686bec1c7bfa17d1 100644
--- a/uffd/password_hash.py
+++ b/uffd/password_hash.py
@@ -2,6 +2,7 @@ import secrets
 import hashlib
 import base64
 from crypt import crypt
+import argon2
 
 def build_value(method_name, data):
 	return '{' + method_name + '}' + data
@@ -179,6 +180,28 @@ class CryptPasswordHash(PasswordHash):
 	def verify(self, password):
 		return secrets.compare_digest(crypt(password, self.data), self.data)
 
+@registry.register
+class Argon2PasswordHash(PasswordHash):
+	METHOD_NAME = 'argon2'
+
+	hasher = argon2.PasswordHasher()
+
+	@classmethod
+	def from_password(cls, password):
+		return cls(build_value(cls.METHOD_NAME, cls.hasher.hash(password)))
+
+	def verify(self, password):
+		try:
+			return self.hasher.verify(self.data, password)
+		except argon2.exceptions.Argon2Error:
+			return False
+		except argon2.exceptions.InvalidHash:
+			return False
+
+	@property
+	def needs_rehash(self):
+		return super().needs_rehash or self.hasher.check_needs_rehash(self.data)
+
 class InvalidPasswordHash:
 	def __init__(self, value=None):
 		self.value = value
@@ -262,9 +285,8 @@ class PasswordHashAttribute:
 		setattr(obj, self.attribute_name, value.value)
 
 # Hashing method for (potentially) low entropy secrets like user passwords. Is
-# usually slow and uses salting to make dictionary attacks difficult. Note
-# that SSHA512 is not slow and should be replaced with a modern alternative.
-LowEntropyPasswordHash = SaltedSHA512PasswordHash
+# usually slow and uses salting to make dictionary attacks difficult.
+LowEntropyPasswordHash = Argon2PasswordHash
 
 # Hashing method for high entropy secrets like API keys. The secrets are
 # generated instead of user-selected to ensure a high level of entropy. Is