diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py
index f36cfe93c42eef6c5cf99cd8f05370fd7a6231f2..da3c94ddd926729d83cb6962a48292fd5f8c4597 100644
--- a/tests/test_selfservice.py
+++ b/tests/test_selfservice.py
@@ -101,6 +101,19 @@ class TestSelfservice(UffdTestCase):
 		self.assertFalse(ldap.test_user_bind(_user.dn, 'shortpw'))
 		self.assertTrue(ldap.test_user_bind(_user.dn, 'userpassword'))
 
+	# Regression test for #100 (login not possible if password contains character disallowed by SASLprep)
+	def test_change_password_samlprep_invalid(self):
+		self.login_as('user')
+		user = request.user
+		r = self.client.post(path=url_for('selfservice.change_password'),
+			data={'password1': 'shortpw\n', 'password2': 'shortpw\n'},
+			follow_redirects=True)
+		dump('change_password_samlprep_invalid', r)
+		self.assertEqual(r.status_code, 200)
+		_user = request.user
+		self.assertFalse(ldap.test_user_bind(_user.dn, 'shortpw\n'))
+		self.assertTrue(ldap.test_user_bind(_user.dn, 'userpassword'))
+
 	def test_change_password_mismatch(self):
 		self.login_as('user')
 		user = request.user
diff --git a/tests/test_user.py b/tests/test_user.py
index 962840db8de6ad9499c7adacdc123b9919caa72b..ea6bb9a6edf363cca20ccdac0e5ff6b7ec6abbc0 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -193,6 +193,7 @@ class TestUserViews(UffdTestCase):
 		self.assertEqual(user_updated.uid, user_unupdated.uid)
 		self.assertEqual(user_updated.loginname, user_unupdated.loginname)
 		self.assertTrue(ldap.test_user_bind(user_updated.dn, 'newpassword'))
+		self.assertFalse(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password')))
 
 	def test_update_invalid_password(self):
 		user_unupdated = self.get_user()
@@ -201,10 +202,28 @@ class TestUserViews(UffdTestCase):
 		r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid),
 			data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User',
 			'password': 'A'}, follow_redirects=True)
-		dump('user_update_password', r)
+		dump('user_update_invalid_password', r)
 		self.assertEqual(r.status_code, 200)
 		user_updated = self.get_user()
 		self.assertFalse(ldap.test_user_bind(user_updated.dn, 'A'))
+		self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password')))
+		self.assertEqual(user_updated.displayname, user_unupdated.displayname)
+		self.assertEqual(user_updated.mail, user_unupdated.mail)
+		self.assertEqual(user_updated.loginname, user_unupdated.loginname)
+
+	# Regression test for #100 (login not possible if password contains character disallowed by SASLprep)
+	def test_update_saslprep_invalid_password(self):
+		user_unupdated = self.get_user()
+		r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid),
+			data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User',
+			'password': 'newpassword\n'}, follow_redirects=True)
+		dump('user_update_saslprep_invalid_password', r)
+		self.assertEqual(r.status_code, 200)
+		user_updated = self.get_user()
+		self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword\n'))
+		self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password')))
 		self.assertEqual(user_updated.displayname, user_unupdated.displayname)
 		self.assertEqual(user_updated.mail, user_unupdated.mail)
 		self.assertEqual(user_updated.loginname, user_unupdated.loginname)
@@ -223,6 +242,7 @@ class TestUserViews(UffdTestCase):
 		self.assertEqual(user_updated.mail, user_unupdated.mail)
 		self.assertEqual(user_updated.loginname, user_unupdated.loginname)
 		self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword'))
+		self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password')))
 
 	def test_update_invalid_display_name(self):
 		user_unupdated = self.get_user()
@@ -238,6 +258,7 @@ class TestUserViews(UffdTestCase):
 		self.assertEqual(user_updated.mail, user_unupdated.mail)
 		self.assertEqual(user_updated.loginname, user_unupdated.loginname)
 		self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword'))
+		self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password')))
 
 	def test_show(self):
 		r = self.client.get(path=url_for('user.show', uid=self.get_user().uid), follow_redirects=True)
diff --git a/uffd/selfservice/templates/selfservice/self.html b/uffd/selfservice/templates/selfservice/self.html
index edee7bdc1d5fe7ab8c93ca2072785f5d6e5c174e..158e3f3dfbc92176b87bb1ceac50960e54690f2c 100644
--- a/uffd/selfservice/templates/selfservice/self.html
+++ b/uffd/selfservice/templates/selfservice/self.html
@@ -47,9 +47,9 @@
 	<div class="col-12 col-md-7">
 		<form class="form" action="{{ url_for("selfservice.change_password") }}" method="POST">
 			<div class="form-group">
-				<input type="password" class="form-control" id="user-password1" name="password1" placeholder="{{_("New Password")}}" required>
+				<input type="password" class="form-control" id="user-password1" name="password1" placeholder="{{_("New Password")}}" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
 				<small class="form-text text-muted">
-					{{_('At least 8 and at most 256 characters, no other special requirements.')}}
+					{{ User.PASSWORD_DESCRIPTION|safe }}
 				</small>
 			</div>
 			<div class="form-group">
diff --git a/uffd/selfservice/templates/selfservice/set_password.html b/uffd/selfservice/templates/selfservice/set_password.html
index 42eaa8660c3f90ac927f51ba2583e981ea8fa46f..ff4c447895b3c52a3beb927e090403e89ed4d567 100644
--- a/uffd/selfservice/templates/selfservice/set_password.html
+++ b/uffd/selfservice/templates/selfservice/set_password.html
@@ -12,14 +12,14 @@
 		</div>
 		<div class="form-group col-12">
 			<label for="user-password1">{{_("New Password")}}</label>
-			<input type="password" class="form-control" id="user-password1" name="password1" required="required" tabindex = "2">
+			<input type="password" class="form-control" id="user-password1" name="password1" tabindex="2" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
 			<small class="form-text text-muted">
-				{{_("At least 8 and at most 256 characters, no other special requirements. But please use a password manager.")}}
+				{{ User.PASSWORD_DESCRIPTION|safe }}
 			</small>
 		</div>
 		<div class="form-group col-12">
 			<label for="user-password2">{{_("Repeat Password")}}</label>
-			<input type="password" class="form-control" id="user-password2" name="password2" required="required" tabindex = "2">
+			<input type="password" class="form-control" id="user-password2" name="password2" tabindex="3" required>
 		</div>
 		<div class="form-group col-12">
 			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">{{_("Set password")}}</button>
diff --git a/uffd/signup/templates/signup/start.html b/uffd/signup/templates/signup/start.html
index a60dbdb2f3270f48f3e1fcff09bf955fd4e74411..ad150f4ba5f538aefb85ba7a6b6cdeceef9e81ec 100644
--- a/uffd/signup/templates/signup/start.html
+++ b/uffd/signup/templates/signup/start.html
@@ -44,9 +44,9 @@
 		</div>
 		<div class="form-group col-12">
 			<label for="user-password1">{{_('Password')}}</label>
-			<input type="password" class="form-control" id="user-password1" name="password1" minlength=8 maxlength=256 required>
+			<input type="password" class="form-control" id="user-password1" name="password1" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
 			<small class="form-text text-muted">
-				{{_("At least 8 and at most 256 characters, no other special requirements. But please use a password manager.")}}
+				{{ User.PASSWORD_DESCRIPTION|safe }}
 			</small>
 		</div>
 		<div class="form-group col-12">
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index 73a0ce629d71d5336446229934354d0b64f25f98..e58966921aed2726349395182b5de736f144f297 100644
Binary files a/uffd/translations/de/LC_MESSAGES/messages.mo and b/uffd/translations/de/LC_MESSAGES/messages.mo differ
diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po
index 3e355080052e5b91ae7a0cb69aacff3069bba8ec..bae852ddaffca378b3c2dd4a5f706f31bdc68363 100644
--- a/uffd/translations/de/LC_MESSAGES/messages.po
+++ b/uffd/translations/de/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2021-08-13 14:24+0200\n"
+"POT-Creation-Date: 2021-08-30 23:22+0200\n"
 "PO-Revision-Date: 2021-05-25 21:18+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: de\n"
@@ -267,7 +267,7 @@ msgstr "Enthaltene Rollen"
 #: uffd/role/templates/role/list.html:14
 #: uffd/rolemod/templates/rolemod/list.html:9
 #: uffd/rolemod/templates/rolemod/show.html:46
-#: uffd/selfservice/templates/selfservice/self.html:103
+#: uffd/selfservice/templates/selfservice/self.html:101
 #: uffd/user/templates/group/list.html:10
 #: uffd/user/templates/group/show.html:11 uffd/user/templates/user/show.html:95
 #: uffd/user/templates/user/show.html:127
@@ -278,7 +278,7 @@ msgstr "Name"
 #: uffd/role/templates/role/list.html:15 uffd/role/templates/role/show.html:48
 #: uffd/rolemod/templates/rolemod/list.html:10
 #: uffd/rolemod/templates/rolemod/show.html:28
-#: uffd/selfservice/templates/selfservice/self.html:104
+#: uffd/selfservice/templates/selfservice/self.html:102
 #: uffd/user/templates/group/list.html:11 uffd/user/templates/user/show.html:96
 #: uffd/user/templates/user/show.html:128
 msgid "Description"
@@ -426,7 +426,7 @@ msgid "Two-factor authentication failed"
 msgstr "Zwei-Faktor-Authentifizierung fehlgeschlagen"
 
 #: uffd/mfa/templates/mfa/auth.html:12
-#: uffd/selfservice/templates/selfservice/self.html:69
+#: uffd/selfservice/templates/selfservice/self.html:67
 msgid "Two-Factor Authentication"
 msgstr "Zwei-Faktor-Authentifizierung"
 
@@ -473,12 +473,12 @@ msgid "Disable two-factor authentication"
 msgstr "Zwei-Faktor-Authentifizierung (2FA) deaktivieren"
 
 #: uffd/mfa/templates/mfa/setup.html:18
-#: uffd/selfservice/templates/selfservice/self.html:75
+#: uffd/selfservice/templates/selfservice/self.html:73
 msgid "Two-factor authentication is currently <strong>enabled</strong>."
 msgstr "Die Zwei-Faktor-Authentifizierung ist derzeit <strong>aktiviert</strong>."
 
 #: uffd/mfa/templates/mfa/setup.html:20
-#: uffd/selfservice/templates/selfservice/self.html:77
+#: uffd/selfservice/templates/selfservice/self.html:75
 msgid "Two-factor authentication is currently <strong>disabled</strong>."
 msgstr ""
 "Die Zwei-Faktor-Authentifizierung ist derzeit "
@@ -758,11 +758,11 @@ msgstr ""
 
 #: uffd/role/views.py:44 uffd/rolemod/views.py:36 uffd/rolemod/views.py:45
 #: uffd/rolemod/views.py:60 uffd/session/views.py:130
-#: uffd/user/views_group.py:14 uffd/user/views_user.py:22
+#: uffd/user/views_group.py:14 uffd/user/views_user.py:25
 msgid "Access denied"
 msgstr "Zugriff verweigert"
 
-#: uffd/role/views.py:51 uffd/selfservice/templates/selfservice/self.html:88
+#: uffd/role/views.py:51 uffd/selfservice/templates/selfservice/self.html:86
 #: uffd/user/templates/user/list.html:20 uffd/user/templates/user/show.html:21
 #: uffd/user/templates/user/show.html:90
 msgid "Roles"
@@ -797,7 +797,7 @@ msgid "Set as default"
 msgstr "Als Default setzen"
 
 #: uffd/role/templates/role/show.html:19 uffd/role/templates/role/show.html:21
-#: uffd/selfservice/templates/selfservice/self.html:119
+#: uffd/selfservice/templates/selfservice/self.html:117
 #: uffd/user/templates/user/show.html:11
 msgid "Are you sure?"
 msgstr "Wirklich fortfahren?"
@@ -980,7 +980,7 @@ msgid "Forgot password"
 msgstr "Passwort vergessen"
 
 #: uffd/selfservice/templates/selfservice/forgot_password.html:14
-#: uffd/selfservice/templates/selfservice/self.html:22
+#: uffd/selfservice/templates/selfservice/self.html:21
 #: uffd/session/templates/session/login.html:14
 #: uffd/signup/templates/signup/start.html:19
 #: uffd/user/templates/user/list.html:18 uffd/user/templates/user/show.html:48
@@ -1023,35 +1023,35 @@ msgstr ""
 msgid "Changes may take serveral minutes to be visible in all services."
 msgstr "Änderungen sind erst nach einigen Minuten in allen Diensten sichtbar."
 
-#: uffd/selfservice/templates/selfservice/self.html:27
+#: uffd/selfservice/templates/selfservice/self.html:25
 #: uffd/signup/templates/signup/start.html:32
 #: uffd/user/templates/user/list.html:19 uffd/user/templates/user/show.html:63
 msgid "Display Name"
 msgstr "Anzeigename"
 
-#: uffd/selfservice/templates/selfservice/self.html:31
+#: uffd/selfservice/templates/selfservice/self.html:29
 #: uffd/signup/templates/signup/start.html:39
 msgid "E-Mail Address"
 msgstr "E-Mail-Adresse"
 
-#: uffd/selfservice/templates/selfservice/self.html:34
+#: uffd/selfservice/templates/selfservice/self.html:32
 msgid "We will send you a confirmation mail to this address if you change it"
 msgstr ""
 "Wir werden dir eine Bestätigungsmail zum Setzen der neuen E-Mail-Adresse "
 "senden."
 
-#: uffd/selfservice/templates/selfservice/self.html:37
+#: uffd/selfservice/templates/selfservice/self.html:35
 msgid "Update Profile"
 msgstr "Änderungen speichern"
 
-#: uffd/selfservice/templates/selfservice/self.html:46
+#: uffd/selfservice/templates/selfservice/self.html:44
 #: uffd/session/templates/session/login.html:18
 #: uffd/signup/templates/signup/start.html:46
 #: uffd/user/templates/user/show.html:77
 msgid "Password"
 msgstr "Passwort"
 
-#: uffd/selfservice/templates/selfservice/self.html:47
+#: uffd/selfservice/templates/selfservice/self.html:45
 msgid ""
 "Your login password for the Single-Sign-On. Only enter it on the Single-"
 "Sign-On login page! No other legit websites will ask you for this "
@@ -1062,27 +1062,22 @@ msgstr ""
 " Webseite wird dich nach diesem Passwort fragen. Es wird auch niemals für"
 " Support-Anfragen benötigt."
 
-#: uffd/selfservice/templates/selfservice/self.html:52
+#: uffd/selfservice/templates/selfservice/self.html:50
 #: uffd/selfservice/templates/selfservice/set_password.html:14
 msgid "New Password"
 msgstr "Neues Passwort"
 
-#: uffd/selfservice/templates/selfservice/self.html:54
-#: uffd/user/templates/user/show.html:84
-msgid "At least 8 and at most 256 characters, no other special requirements."
-msgstr "Mindestens 8 und maximal 256 Zeichen, keine weiteren Einschränkungen."
-
-#: uffd/selfservice/templates/selfservice/self.html:58
+#: uffd/selfservice/templates/selfservice/self.html:56
 #: uffd/selfservice/templates/selfservice/set_password.html:21
 #: uffd/signup/templates/signup/start.html:53
 msgid "Repeat Password"
 msgstr "Passwort wiederholen"
 
-#: uffd/selfservice/templates/selfservice/self.html:60
+#: uffd/selfservice/templates/selfservice/self.html:58
 msgid "Change Password"
 msgstr "Passwort ändern"
 
-#: uffd/selfservice/templates/selfservice/self.html:70
+#: uffd/selfservice/templates/selfservice/self.html:68
 msgid ""
 "Setting up Two-Factor Authentication (2FA) adds an additional step to the"
 " Single-Sign-On login and increases the security of your account "
@@ -1092,11 +1087,11 @@ msgstr ""
 "Anmeldung im Single-Sign-On hinzu und verbessert damit die Sicherheit "
 "deines Accounts erheblich."
 
-#: uffd/selfservice/templates/selfservice/self.html:80
+#: uffd/selfservice/templates/selfservice/self.html:78
 msgid "Manage two-factor authentication"
 msgstr "Zwei-Faktor-Authentifizierung (2FA) verwalten"
 
-#: uffd/selfservice/templates/selfservice/self.html:89
+#: uffd/selfservice/templates/selfservice/self.html:87
 msgid ""
 "Aside from a set of base permissions, your roles determine the "
 "permissions of your account."
@@ -1104,7 +1099,7 @@ msgstr ""
 "Deine Berechtigungen werden, von einigen Basis-Berechtigungen abgesehen, "
 "von deinen Rollen bestimmt"
 
-#: uffd/selfservice/templates/selfservice/self.html:91
+#: uffd/selfservice/templates/selfservice/self.html:89
 #, python-format
 msgid ""
 "See <a href=\"%(services_url)s\">Services</a> for an overview of your "
@@ -1113,17 +1108,17 @@ msgstr ""
 "Auf <a href=\"%(services_url)s\">Dienste</a> erhälst du einen Überblick "
 "über deine aktuellen Berechtigungen."
 
-#: uffd/selfservice/templates/selfservice/self.html:96
+#: uffd/selfservice/templates/selfservice/self.html:94
 msgid "Administrators and role moderators can invite you to new roles."
 msgstr ""
 "Accounts mit Adminrechten oder Rollen-Moderationsrechten können dich zu "
 "Rollen einladen."
 
-#: uffd/selfservice/templates/selfservice/self.html:98
+#: uffd/selfservice/templates/selfservice/self.html:96
 msgid "Administrators can add new roles to your account."
 msgstr "Accounts mit Adminrechten können dich zu neuen Rollen hinzufügen."
 
-#: uffd/selfservice/templates/selfservice/self.html:113
+#: uffd/selfservice/templates/selfservice/self.html:111
 msgid ""
 "Some permissions in this role require you to setup two-factor "
 "authentication"
@@ -1131,11 +1126,11 @@ msgstr ""
 "Einige Berechtigungen dieser Rolle erfordern das Einrichten von Zwei-"
 "Faktor-Authentifikation"
 
-#: uffd/selfservice/templates/selfservice/self.html:120
+#: uffd/selfservice/templates/selfservice/self.html:118
 msgid "Leave"
 msgstr "Verlassen"
 
-#: uffd/selfservice/templates/selfservice/self.html:128
+#: uffd/selfservice/templates/selfservice/self.html:126
 msgid "You currently don't have any roles"
 msgstr "Du hast derzeit keine Rollen"
 
@@ -1143,15 +1138,6 @@ msgstr "Du hast derzeit keine Rollen"
 msgid "Reset password"
 msgstr "Passwort zurücksetzen"
 
-#: uffd/selfservice/templates/selfservice/set_password.html:17
-#: uffd/signup/templates/signup/start.html:49
-msgid ""
-"At least 8 and at most 256 characters, no other special requirements. But"
-" please use a password manager."
-msgstr ""
-"Mindestens 8 und maximal 256 Zeichen, keine weiteren Einschränkungen. "
-"Bitte verwende einen Passwort-Manager."
-
 #: uffd/selfservice/templates/selfservice/set_password.html:25
 msgid "Set password"
 msgstr "Passwort setzen"
@@ -1417,45 +1403,56 @@ msgstr "Ändern"
 msgid "About uffd"
 msgstr "Über uffd"
 
+#: uffd/user/models.py:52
+#, python-format
+msgid ""
+"At least %(minlen)d and at most %(maxlen)d characters. Only letters, "
+"digits, spaces and some symbols (<code>%(symbols)s</code>) allowed. "
+"Please use a password manager."
+msgstr ""
+"%(minlen)d bis %(maxlen)d Zeichen. Nur Buchstaben, Ziffern, Leerzeichen "
+"und manche Symbole (<code>%(symbols)s</code>), keine Umlaute. Bitte "
+"verwende einen Passwort-Manager."
+
 #: uffd/user/views_group.py:21
 msgid "Groups"
 msgstr "Gruppen"
 
-#: uffd/user/views_user.py:29
+#: uffd/user/views_user.py:32
 msgid "Users"
 msgstr "Accounts"
 
-#: uffd/user/views_user.py:49
+#: uffd/user/views_user.py:52
 msgid "Login name does not meet requirements"
 msgstr "Anmeldename entspricht nicht den Anforderungen"
 
-#: uffd/user/views_user.py:54
+#: uffd/user/views_user.py:57
 msgid "Mail is invalid"
 msgstr "E-Mail-Adresse nicht valide"
 
-#: uffd/user/views_user.py:58
+#: uffd/user/views_user.py:61
 msgid "Display name does not meet requirements"
 msgstr "Anzeigename entspricht nicht den Anforderungen"
 
-#: uffd/user/views_user.py:63
+#: uffd/user/views_user.py:66
 msgid "Password is invalid"
 msgstr "Passwort ist ungültig"
 
-#: uffd/user/views_user.py:77
+#: uffd/user/views_user.py:80
 msgid "Service user created"
 msgstr "Service-Account erstellt"
 
-#: uffd/user/views_user.py:80
+#: uffd/user/views_user.py:83
 msgid "User created. We sent the user a password reset link by mail"
 msgstr ""
 "Account erstellt. E-Mail mit einem Link zum Setzen des Passworts wurde "
 "versendet."
 
-#: uffd/user/views_user.py:82
+#: uffd/user/views_user.py:85
 msgid "User updated"
 msgstr "Account aktualisiert"
 
-#: uffd/user/views_user.py:93
+#: uffd/user/views_user.py:96
 msgid "Deleted user"
 msgstr "Account gelöscht"
 
diff --git a/uffd/user/models.py b/uffd/user/models.py
index 81a6e3638aae7b3f3687f0bd5d88ef60ed7e187e..3a3069f758efd45090c2b17a751a8cbe4c4a5e19 100644
--- a/uffd/user/models.py
+++ b/uffd/user/models.py
@@ -2,7 +2,8 @@ import secrets
 import string
 import re
 
-from flask import current_app
+from flask import current_app, escape
+from flask_babel import lazy_gettext
 from ldap3.utils.hashed import hashed, HASHED_SALTED_SHA512
 
 from uffd.ldap import ldap
@@ -36,6 +37,20 @@ def format_with_attributes(fmtstr, obj):
 	return fmtstr.format_map(ObjectAttributeDict(obj))
 
 class BaseUser(ldap.Model):
+	# Allows 8 to 256 ASCII letters (lower and upper case), digits, spaces and
+	# symbols/punctuation characters. It disallows control characters and
+	# non-ASCII characters to prevent setting passwords considered invalid by
+	# SASLprep.
+	#
+	# This REGEX ist used both in Python and JS.
+	PASSWORD_REGEX = '[ -~]*'
+	PASSWORD_MINLEN = 8
+	PASSWORD_MAXLEN = 256
+	PASSWORD_DESCRIPTION = lazy_gettext('At least %(minlen)d and at most %(maxlen)d characters. ' + \
+	                                    'Only letters, digits, spaces and some symbols (<code>%(symbols)s</code>) allowed. ' + \
+	                                    'Please use a password manager.',
+	                                    minlen=PASSWORD_MINLEN, maxlen=PASSWORD_MAXLEN, symbols=escape('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'))
+
 	ldap_search_base = lazyconfig_str('LDAP_USER_SEARCH_BASE')
 	ldap_filter_params = lazyconfig_list('LDAP_USER_SEARCH_FILTER')
 	ldap_object_classes = lazyconfig_list('LDAP_USER_OBJECTCLASSES')
@@ -129,7 +144,7 @@ class BaseUser(ldap.Model):
 		return True
 
 	def set_password(self, value):
-		if len(value) < 8 or len(value) > 256:
+		if len(value) < self.PASSWORD_MINLEN or len(value) > self.PASSWORD_MAXLEN or not re.fullmatch(self.PASSWORD_REGEX, value):
 			return False
 		self.password = value
 		return True
diff --git a/uffd/user/templates/user/show.html b/uffd/user/templates/user/show.html
index 59ef1bd1d0cbaa3d37db4ae62b7554262866f1c9..052dd09a866565df14f7bff81533fa48bf1d81e7 100644
--- a/uffd/user/templates/user/show.html
+++ b/uffd/user/templates/user/show.html
@@ -76,12 +76,12 @@
 			<div class="form-group col">
 				<label for="user-loginname">{{_("Password")}}</label>
 				{% if user.uid %}
-				<input type="password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●">
+				<input type="password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}">
 				{% else %}
 				<input type="password" class="form-control" id="user-password" name="password" placeholder="{{_("mail to set it will be sent")}}" readonly>
 				{% endif %}
 				<small class="form-text text-muted">
-					{{_("At least 8 and at most 256 characters, no other special requirements.")}}
+					{{ User.PASSWORD_DESCRIPTION|safe }}
 				</small>
 			</div>
 		</div>
diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py
index ee5bc71eed7ee34364f64aa2c092fe94465a315f..2b2739465cc9e376afb73bc57d34e1fde790caa0 100644
--- a/uffd/user/views_user.py
+++ b/uffd/user/views_user.py
@@ -15,6 +15,9 @@ from uffd.ldap import ldap, LDAPCommitError
 from .models import User
 
 bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/')
+
+bp.add_app_template_global(User, 'User')
+
 @bp.before_request
 @login_required()
 def user_acl(): #pylint: disable=inconsistent-return-statements