diff --git a/tests/test_mfa.py b/tests/test_mfa.py
index 0157ad532567ffde44cc7be4a680d27f2dc65731..e12469b16a69dad5495224b8723b0b1c0a571dcf 100644
--- a/tests/test_mfa.py
+++ b/tests/test_mfa.py
@@ -8,6 +8,7 @@ from flask import url_for, session, request
 from uffd import ldap, user
 
 from uffd.user.models import User
+from uffd.role.models import Role, RoleGroup
 from uffd.mfa.models import MFAMethod, MFAType, RecoveryCodeMethod, TOTPMethod, WebauthnMethod, _hotp
 from uffd import create_app, db
 
@@ -155,6 +156,10 @@ class TestMfaViews(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 
 	def test_disable(self):
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
+		db.session.commit()
 		self.login_as('user')
 		self.add_recovery_codes()
 		self.add_totp()
@@ -169,6 +174,10 @@ class TestMfaViews(UffdTestCase):
 		self.assertEqual(len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()), admin_methods)
 
 	def test_disable_recovery_only(self):
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
+		db.session.commit()
 		self.login_as('user')
 		self.add_recovery_codes()
 		admin_methods = len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all())
@@ -227,6 +236,10 @@ class TestMfaViews(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 
 	def test_setup_totp_finish(self):
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
+		db.session.commit()
 		self.login_as('user')
 		self.add_recovery_codes()
 		self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0)
@@ -273,6 +286,10 @@ class TestMfaViews(UffdTestCase):
 		self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0)
 
 	def test_delete_totp(self):
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
+		db.session.commit()
 		self.login_as('user')
 		self.add_recovery_codes()
 		self.add_totp()
diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py
index c7c4b14c8ddeb6fbd1d020b3aed1241306a808f6..bb5075b20b59af03542e3f31ef538b78c2123179 100644
--- a/tests/test_selfservice.py
+++ b/tests/test_selfservice.py
@@ -8,7 +8,7 @@ from uffd import ldap, user
 
 from uffd.selfservice.models import MailToken, PasswordToken
 from uffd.user.models import User
-from uffd.role.models import Role
+from uffd.role.models import Role, RoleGroup
 from uffd import create_app, db
 
 from utils import dump, UffdTestCase
@@ -130,6 +130,9 @@ class TestSelfservice(UffdTestCase):
 	def test_leave_role(self):
 		if self.use_userconnection:
 			self.skipTest('Leaving roles is not possible in user mode')
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
 		role1 = Role(name='testrole1')
 		role2 = Role(name='testrole2')
 		db.session.add(role1)
diff --git a/tests/test_session.py b/tests/test_session.py
index 312a66b42e3585aa6ae7287438c5b83863ac32dd..c7707ea3f6644841d230a1a561a1576e6cabc4d4 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -23,12 +23,12 @@ class TestSession(UffdTestCase):
 			return 'SUCCESS', 200
 
 		@self.app.route('/test_group_required1')
-		@login_required(group='users')
+		@login_required(lambda: request.user.is_in_group('users'))
 		def test_group_required1():
 			return 'SUCCESS', 200
 
 		@self.app.route('/test_group_required2')
-		@login_required(group='notagroup')
+		@login_required(lambda: request.user.is_in_group('notagroup'))
 		def test_group_required2():
 			return 'SUCCESS', 200
 
diff --git a/tests/test_signup.py b/tests/test_signup.py
index 3b940131e672f88e7ce3ea604b060094e5d86b92..f6b65275b4aa9ef988d3c010c970eb0747679110 100644
--- a/tests/test_signup.py
+++ b/tests/test_signup.py
@@ -11,6 +11,7 @@ from uffd.ldap import ldap
 from uffd import create_app, db
 from uffd.signup.models import Signup
 from uffd.user.models import User
+from uffd.role.models import Role, RoleGroup
 from uffd.session.views import login_get_user
 
 from utils import dump, UffdTestCase, db_flush
@@ -323,6 +324,10 @@ class TestSignupViews(UffdTestCase):
 		self.assertEqual(r.json['status'], 'ratelimited')
 
 	def test_confirm(self):
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
+		db.session.commit()
 		signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
 		signup = refetch_signup(signup)
 		self.assertFalse(signup.completed)
@@ -349,6 +354,10 @@ class TestSignupViews(UffdTestCase):
 		self.assertEqual(request.user.loginname, 'newuser')
 
 	def test_confirm_loggedin(self):
+		baserole = Role(name='baserole', is_default=True)
+		db.session.add(baserole)
+		baserole.groups[self.get_access_group()] = RoleGroup()
+		db.session.commit()
 		signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
 		signup = refetch_signup(signup)
 		self.login_as('user')
diff --git a/uffd/__init__.py b/uffd/__init__.py
index 90feeafd1185b87bb1f38797da8f37aab62a8a82..fffb779d0a720cf93f0d1a4e3ac4c43e9eaecbcf 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -59,6 +59,8 @@ def create_app(test_config=None): # pylint: disable=too-many-locals
 		for cfg_name in ["config.cfg", "config.json", "config.yml", "config.yaml"]:
 			if load_config_file(app, cfg_name, silent=True):
 				break
+	# Prior to v1.1 login required ACL_SELFSERVICE_GROUP and ACL_ACCESS_GROUP did not exist
+	app.config.setdefault('ACL_ACCESS_GROUP', app.config['ACL_SELFSERVICE_GROUP'])
 
 	register_template_helper(app)
 	setup_navbar(app)
diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index e7ee0d6276b569c79c4b56e3697d053e6ec42cbd..1db33f3502d28df8bc971abfb3943a921a110a2e 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -60,7 +60,10 @@ LANGUAGES={
 }
 
 ACL_ADMIN_GROUP="uffd_admin"
+# Group required to access selfservice functions (view selfservice, change profile/password/roles)
 ACL_SELFSERVICE_GROUP="uffd_access"
+# Group required to login
+#ACL_ACCESS_GROUP="uffd_access" # if unset, the value of ACL_SELFSERVICE_GROUP is used
 # Members can create invite links for signup
 ACL_SIGNUP_GROUP="uffd_signup"
 
diff --git a/uffd/invite/views.py b/uffd/invite/views.py
index 65f4c51c0358e8c1f999c623b417ac34b638d7d7..2c0b7f2754f7af93849fdf86542f82eaaaa348f3 100644
--- a/uffd/invite/views.py
+++ b/uffd/invite/views.py
@@ -1,7 +1,6 @@
 import datetime
-import functools
 
-from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, abort
+from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
 from flask_babel import gettext as _, lazy_gettext
 import sqlalchemy
 
@@ -16,10 +15,11 @@ from uffd.sendmail import sendmail
 from uffd.navbar import register_navbar
 from uffd.ratelimit import host_ratelimit, format_delay
 from uffd.signup.views import signup_ratelimit
+from uffd.selfservice.views import selfservice_acl_check
 
 bp = Blueprint('invite', __name__, template_folder='templates', url_prefix='/invite/')
 
-def invite_acl():
+def invite_acl_check():
 	if not request.user:
 		return False
 	if request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
@@ -30,15 +30,6 @@ def invite_acl():
 		return True
 	return False
 
-def invite_acl_required(func):
-	@functools.wraps(func)
-	@login_required()
-	def decorator(*args, **kwargs):
-		if not invite_acl():
-			abort(403)
-		return func(*args, **kwargs)
-	return decorator
-
 def view_acl_filter(user):
 	if user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
 		return sqlalchemy.true()
@@ -52,14 +43,14 @@ def reset_acl_filter(user):
 	return Invite.creator_dn == user.dn
 
 @bp.route('/')
-@register_navbar(14, lazy_gettext('Invites'), icon='link', blueprint=bp, visible=invite_acl)
-@invite_acl_required
+@register_navbar(14, lazy_gettext('Invites'), icon='link', blueprint=bp, visible=invite_acl_check)
+@login_required(invite_acl_check)
 def index():
 	invites = Invite.query.filter(view_acl_filter(request.user)).all()
 	return render_template('invite/list.html', invites=invites)
 
 @bp.route('/new')
-@invite_acl_required
+@login_required(invite_acl_check)
 def new():
 	if request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
 		allow_signup = True
@@ -70,7 +61,7 @@ def new():
 	return render_template('invite/new.html', roles=roles, allow_signup=allow_signup)
 
 @bp.route('/new', methods=['POST'])
-@invite_acl_required
+@login_required(invite_acl_check)
 @csrf_protect(blueprint=bp)
 def new_submit():
 	invite = Invite(creator=request.user,
@@ -94,7 +85,7 @@ def new_submit():
 	return redirect(url_for('invite.index'))
 
 @bp.route('/<int:invite_id>/disable', methods=['POST'])
-@invite_acl_required
+@login_required(invite_acl_check)
 @csrf_protect(blueprint=bp)
 def disable(invite_id):
 	invite = Invite.query.filter(view_acl_filter(request.user)).filter_by(id=invite_id).first_or_404()
@@ -103,7 +94,7 @@ def disable(invite_id):
 	return redirect(url_for('.index'))
 
 @bp.route('/<int:invite_id>/reset', methods=['POST'])
-@invite_acl_required
+@login_required(invite_acl_check)
 @csrf_protect(blueprint=bp)
 def reset(invite_id):
 	invite = Invite.query.filter(reset_acl_filter(request.user)).filter_by(id=invite_id).first_or_404()
@@ -120,7 +111,7 @@ def use(token):
 	return render_template('invite/use.html', invite=invite)
 
 @bp.route('/<token>/grant', methods=['POST'])
-@login_required()
+@login_required(selfservice_acl_check)
 @csrf_protect(blueprint=bp)
 def grant(token):
 	invite = Invite.query.filter_by(token=token).first_or_404()
diff --git a/uffd/mail/views.py b/uffd/mail/views.py
index 2e11905b0dce159ba1ee4c3d60329735c9817ce4..9f5f2e1d2f22410b59de7ea375fb088339b2a0f7 100644
--- a/uffd/mail/views.py
+++ b/uffd/mail/views.py
@@ -1,4 +1,4 @@
-from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
 from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
@@ -9,15 +9,15 @@ from uffd.session import login_required
 from uffd.mail.models import Mail
 
 bp = Blueprint("mail", __name__, template_folder='templates', url_prefix='/mail/')
-@bp.before_request
-@login_required()
-def mail_acl():
-	if not mail_acl_check():
-		abort(403)
 
 def mail_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
+@bp.before_request
+@login_required(mail_acl_check)
+def mail_acl():
+	pass
+
 @bp.route("/")
 @register_navbar(29, lazy_gettext('Forwardings'), icon='envelope', blueprint=bp, visible=mail_acl_check)
 def index():
diff --git a/uffd/role/views.py b/uffd/role/views.py
index 0b3e827f2a65d16cef7031fc98153d7769cb1a90..7261c63ff08c0939292533e8b063ef2d2e6ac8b8 100644
--- a/uffd/role/views.py
+++ b/uffd/role/views.py
@@ -1,6 +1,6 @@
 import sys
 
-from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
 from flask_babel import gettext as _, lazy_gettext
 import click
 
@@ -37,15 +37,14 @@ def add_cli_commands(state):
 				print('Error: LDAP groups are not consistent with roles in database')
 				sys.exit(1)
 
-@bp.before_request
-@login_required()
-def role_acl():
-	if not role_acl_check():
-		abort(403)
-
 def role_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
+@bp.before_request
+@login_required(role_acl_check)
+def role_acl():
+	pass
+
 @bp.route("/")
 @register_navbar(25, lazy_gettext('Roles'), icon='key', blueprint=bp, visible=role_acl_check)
 def index():
diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py
index 1ee8c668dd16897a699fd95527e3a3adc3bfce26..66274b98dc9818b0f9dd76927a0baff91410a209 100644
--- a/uffd/selfservice/views.py
+++ b/uffd/selfservice/views.py
@@ -18,15 +18,18 @@ bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix=
 
 reset_ratelimit = Ratelimit('passwordreset', 1*60*60, 3)
 
+def selfservice_acl_check():
+	return request.user and request.user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP'])
+
 @bp.route("/")
-@register_navbar(0, lazy_gettext('Selfservice'), icon='portrait', blueprint=bp, visible=lambda: bool(request.user))
-@login_required()
+@register_navbar(0, lazy_gettext('Selfservice'), icon='portrait', blueprint=bp, visible=selfservice_acl_check)
+@login_required(selfservice_acl_check)
 def index():
 	return render_template('selfservice/self.html', user=request.user)
 
 @bp.route("/updateprofile", methods=(['POST']))
 @csrf_protect(blueprint=bp)
-@login_required()
+@login_required(selfservice_acl_check)
 def update_profile():
 	user = request.user
 	if request.values['displayname'] != user.displayname:
@@ -42,7 +45,7 @@ def update_profile():
 
 @bp.route("/changepassword", methods=(['POST']))
 @csrf_protect(blueprint=bp)
-@login_required()
+@login_required(selfservice_acl_check)
 def change_password():
 	password_changed = False
 	user = request.user
@@ -79,7 +82,7 @@ def forgot_password():
 	host_ratelimit.log()
 	flash(_("We sent a mail to this user's mail address if you entered the correct mail and login name combination"))
 	user = User.query.filter_by(loginname=loginname).one_or_none()
-	if user and user.mail == mail:
+	if user and user.mail == mail and user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']):
 		send_passwordreset(user)
 	return redirect(url_for('session.login'))
 
@@ -101,6 +104,8 @@ def token_password(token):
 		flash(_('Passwords do not match, please try again.'))
 		return render_template('selfservice/set_password.html', token=token)
 	user = User.query.filter_by(loginname=dbtoken.loginname).one()
+	if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']):
+		abort(403)
 	if not user.set_password(request.values['password1']):
 		flash(_('Password ist not valid, please try again.'))
 		return render_template('selfservice/set_password.html', token=token)
@@ -111,7 +116,7 @@ def token_password(token):
 	return redirect(url_for('session.login'))
 
 @bp.route("/token/mail_verification/<token>")
-@login_required()
+@login_required(selfservice_acl_check)
 def token_mail(token):
 	dbtoken = MailToken.query.get(token)
 	if not dbtoken or dbtoken.created < (datetime.datetime.now() - datetime.timedelta(days=2)):
@@ -133,7 +138,7 @@ def token_mail(token):
 
 @bp.route("/leaverole/<int:roleid>", methods=(['POST']))
 @csrf_protect(blueprint=bp)
-@login_required()
+@login_required(selfservice_acl_check)
 def leave_role(roleid):
 	if not current_app.config['ENABLE_ROLESELFSERVICE']:
 		flash(_('Leaving roles is disabled'))
diff --git a/uffd/session/views.py b/uffd/session/views.py
index 5e8433b038fbfbb746976c37f15ee8046f091e4b..76a5d660c9fa362960e9b73f324433145670b94f 100644
--- a/uffd/session/views.py
+++ b/uffd/session/views.py
@@ -28,6 +28,8 @@ def set_request_user():
 	if datetime.datetime.now().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']:
 		return
 	user = User.query.get(session['user_dn'])
+	if not user.is_in_group(current_app.config['ACL_ACCESS_GROUP']):
+		return
 	request.user_pre_mfa = user
 	if session.get('user_mfa'):
 		request.user = user
@@ -100,7 +102,7 @@ def login():
 		host_ratelimit.log()
 		flash(_('Login name or password is wrong'))
 		return render_template('session/login.html', ref=request.values.get('ref'))
-	if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']):
+	if not user.is_in_group(current_app.config['ACL_ACCESS_GROUP']):
 		flash(_('You do not have access to this service'))
 		return render_template('session/login.html', ref=request.values.get('ref'))
 	set_session(user, password=password)
@@ -119,7 +121,7 @@ def login_required_pre_mfa(no_redirect=False):
 		return decorator
 	return wrapper
 
-def login_required(group=None):
+def login_required(permission_check=lambda: True):
 	def wrapper(func):
 		@functools.wraps(func)
 		def decorator(*args, **kwargs):
@@ -128,7 +130,7 @@ def login_required(group=None):
 				return redirect(url_for('session.login', ref=request.full_path))
 			if not request.user:
 				return redirect(url_for('mfa.auth', ref=request.full_path))
-			if not request.user.is_in_group(group):
+			if not permission_check():
 				abort(403)
 			return func(*args, **kwargs)
 		return decorator
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index 73bf24fe50496a1513b4d3dd910f143915714e7a..9a19ad476f876ce75968e81163d0743da932f670 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 da0a5199fc994833bcc9c085b0cfe810f62bdde9..a1673d29c16597841da10a58e72d509734367619 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-09-05 00:47+0200\n"
+"POT-Creation-Date: 2021-09-05 01:02+0200\n"
 "PO-Revision-Date: 2021-05-25 21:18+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: de\n"
@@ -44,54 +44,54 @@ msgstr "eine Stunde"
 msgid "%(hours)d hours"
 msgstr "%(hours)d Stunden\""
 
-#: uffd/invite/views.py:55
+#: uffd/invite/views.py:46
 msgid "Invites"
 msgstr "Einladungslinks"
 
-#: uffd/invite/views.py:84
+#: uffd/invite/views.py:75
 msgid "The \"Expires After\" date is too far in the future"
 msgstr "Das Ablaufdatum liegt zu weit in der Zukunft"
 
-#: uffd/invite/views.py:87
+#: uffd/invite/views.py:78
 msgid "You are not allowed to create invite links with these permissions"
 msgstr "Dir fehlen Berechtigungen um diesen Einladungslink zu erstellen"
 
-#: uffd/invite/views.py:90
+#: uffd/invite/views.py:81
 msgid "Invite link must either allow signup or grant at least one role"
 msgstr ""
 "Einladungslink muss entweder Account-Registrierung erlauben oder Rollen "
 "vergeben"
 
-#: uffd/invite/views.py:118 uffd/invite/views.py:147
+#: uffd/invite/views.py:109 uffd/invite/views.py:138
 msgid "Invalid invite link"
 msgstr "Ungültiger Einladungslink"
 
-#: uffd/invite/views.py:135
+#: uffd/invite/views.py:126
 msgid "Roles successfully updated"
 msgstr "Rollen erfolgreich geändert"
 
-#: uffd/invite/views.py:150
+#: uffd/invite/views.py:141
 msgid "Invite link does not allow signup"
 msgstr "Einladungslink erlaubt keine Account-Registrierung"
 
-#: uffd/invite/views.py:172 uffd/selfservice/views.py:50
+#: uffd/invite/views.py:163 uffd/selfservice/views.py:53
 #: uffd/signup/views.py:49
 msgid "Passwords do not match"
 msgstr "Die Passwörter stimmen nicht überein"
 
-#: uffd/invite/views.py:177 uffd/signup/views.py:54
+#: uffd/invite/views.py:168 uffd/signup/views.py:54
 #, python-format
 msgid "Too many signup requests with this mail address! Please wait %(delay)s."
 msgstr ""
 "Zu viele Account-Registrierungen mit dieser E-Mail-Adresse! Bitte warte "
 "%(delay)s."
 
-#: uffd/invite/views.py:179 uffd/signup/views.py:56 uffd/signup/views.py:92
+#: uffd/invite/views.py:170 uffd/signup/views.py:56 uffd/signup/views.py:92
 #, python-format
 msgid "Too many requests! Please wait %(delay)s."
 msgstr "Zu viele Anfragen! Bitte warte %(delay)s."
 
-#: uffd/invite/views.py:192 uffd/signup/views.py:68
+#: uffd/invite/views.py:183 uffd/signup/views.py:68
 msgid "Cound not send mail"
 msgstr "Mailversand fehlgeschlagen"
 
@@ -704,8 +704,8 @@ msgstr "Verifiziere und beende das Setup"
 msgid "You need to login to access this service"
 msgstr "Du musst dich anmelden, um auf diesen Dienst zugreifen zu können"
 
-#: uffd/oauth2/views.py:140 uffd/selfservice/views.py:76
-#: uffd/session/views.py:95
+#: uffd/oauth2/views.py:140 uffd/selfservice/views.py:79
+#: uffd/session/views.py:97
 #, python-format
 msgid ""
 "We received too many requests from your ip address/network! Please wait "
@@ -773,13 +773,13 @@ msgstr ""
 "Automatisches Abmelden bei einigen Diensten fehlgeschlagen. Nochmal "
 "versuchen?"
 
-#: uffd/role/views.py:50 uffd/selfservice/templates/selfservice/self.html:86
+#: uffd/role/views.py:49 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"
 msgstr "Rollen"
 
-#: uffd/role/views.py:100
+#: uffd/role/views.py:99
 msgid "Locked roles cannot be deleted"
 msgstr "Gesperrte Rollen können nicht gelöscht werden"
 
@@ -907,31 +907,31 @@ msgstr "Mitglieder:"
 msgid "Remove"
 msgstr "Entfernen"
 
-#: uffd/selfservice/views.py:22
+#: uffd/selfservice/views.py:25
 msgid "Selfservice"
 msgstr "Selfservice"
 
-#: uffd/selfservice/views.py:34
+#: uffd/selfservice/views.py:37
 msgid "Display name changed."
 msgstr "Anzeigename geändert."
 
-#: uffd/selfservice/views.py:36
+#: uffd/selfservice/views.py:39
 msgid "Display name is not valid."
 msgstr "Anzeigename ist nicht valide."
 
-#: uffd/selfservice/views.py:39
+#: uffd/selfservice/views.py:42
 msgid "We sent you an email, please verify your mail address."
 msgstr "Wir haben dir eine E-Mail gesendet, bitte prüfe deine E-Mail-Adresse."
 
-#: uffd/selfservice/views.py:53
+#: uffd/selfservice/views.py:56
 msgid "Password changed"
 msgstr "Passwort geändert"
 
-#: uffd/selfservice/views.py:56
+#: uffd/selfservice/views.py:59
 msgid "Invalid password"
 msgstr "Passwort ungültig"
 
-#: uffd/selfservice/views.py:74
+#: uffd/selfservice/views.py:77
 #, python-format
 msgid ""
 "We received too many password reset requests for this user! Please wait "
@@ -940,7 +940,7 @@ msgstr ""
 "Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account! "
 "Bitte warte mindestens %(delay)s."
 
-#: uffd/selfservice/views.py:80
+#: uffd/selfservice/views.py:83
 msgid ""
 "We sent a mail to this user's mail address if you entered the correct "
 "mail and login name combination"
@@ -948,27 +948,27 @@ msgstr ""
 "Falls E-Mail-Adresse und Anmeldename richtig waren, wurde eine E-Mail an "
 "die Adresse gesendet."
 
-#: uffd/selfservice/views.py:90 uffd/selfservice/views.py:118
+#: uffd/selfservice/views.py:93 uffd/selfservice/views.py:123
 msgid "Token expired, please try again."
 msgstr "Link abgelaufen, bitte versuche es erneut."
 
-#: uffd/selfservice/views.py:98
+#: uffd/selfservice/views.py:101
 msgid "You need to set a password, please try again."
 msgstr "Password fehlt, bitte versuche es erneut."
 
-#: uffd/selfservice/views.py:101
+#: uffd/selfservice/views.py:104
 msgid "Passwords do not match, please try again."
 msgstr "Die Passwörter stimmen nicht überein, bitte versuche es erneut"
 
-#: uffd/selfservice/views.py:105
+#: uffd/selfservice/views.py:110
 msgid "Password ist not valid, please try again."
 msgstr "Ungültiges Passwort, bitte versuche es erneut"
 
-#: uffd/selfservice/views.py:108
+#: uffd/selfservice/views.py:113
 msgid "New password set"
 msgstr "Passwort geändert"
 
-#: uffd/selfservice/views.py:126
+#: uffd/selfservice/views.py:131
 msgid ""
 "This link was generated for another user. Login as the correct user to "
 "continue."
@@ -976,20 +976,20 @@ msgstr ""
 "Dieser Link wurde für einen anderen Account erstellt. Melde dich mit dem "
 "richtigen Account an um Fortzufahren."
 
-#: uffd/selfservice/views.py:128
+#: uffd/selfservice/views.py:133
 msgid "New mail set"
 msgstr "E-Mail-Adresse geändert"
 
-#: uffd/selfservice/views.py:139
+#: uffd/selfservice/views.py:144
 msgid "Leaving roles is disabled"
 msgstr "Verlassen von Rollen ist deaktiviert"
 
-#: uffd/selfservice/views.py:146
+#: uffd/selfservice/views.py:151
 #, python-format
 msgid "You left role %(role_name)s"
 msgstr "Rolle %(role_name)s verlassen"
 
-#: uffd/selfservice/views.py:163 uffd/selfservice/views.py:183
+#: uffd/selfservice/views.py:168 uffd/selfservice/views.py:188
 #, python-format
 msgid "Mail to \"%(mail_address)s\" could not be sent!"
 msgstr "E-Mail an \"%(mail_address)s\" konnte nicht gesendet werden!"
@@ -1182,7 +1182,7 @@ msgstr "Kein Zugriff"
 msgid "Close"
 msgstr "Schließen"
 
-#: uffd/session/views.py:93
+#: uffd/session/views.py:95
 #, python-format
 msgid ""
 "We received too many invalid login attempts for this user! Please wait at"
@@ -1191,27 +1191,27 @@ msgstr ""
 "Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account "
 "erhalten! Bitte warte mindestens %(delay)s."
 
-#: uffd/session/views.py:101
+#: uffd/session/views.py:103
 msgid "Login name or password is wrong"
 msgstr "Der Anmeldename oder das Passwort ist falsch"
 
-#: uffd/session/views.py:104
+#: uffd/session/views.py:106
 msgid "You do not have access to this service"
 msgstr "Du hast keinen Zugriff auf diesen Service"
 
-#: uffd/session/views.py:116 uffd/session/views.py:127
+#: uffd/session/views.py:118 uffd/session/views.py:129
 msgid "You need to login first"
 msgstr "Du musst dich erst anmelden"
 
-#: uffd/session/views.py:148 uffd/session/views.py:158
+#: uffd/session/views.py:150 uffd/session/views.py:160
 msgid "Initiation code is no longer valid"
 msgstr "Startcode ist nicht mehr gültig"
 
-#: uffd/session/views.py:162
+#: uffd/session/views.py:164
 msgid "Invalid confirmation code"
 msgstr "Ungültiger Bestätigungscode"
 
-#: uffd/session/views.py:174 uffd/session/views.py:185
+#: uffd/session/views.py:176 uffd/session/views.py:187
 msgid "Invalid initiation code"
 msgstr "Ungültiger Startcode"
 
@@ -1445,41 +1445,41 @@ msgstr ""
 msgid "Groups"
 msgstr "Gruppen"
 
-#: uffd/user/views_user.py:31
+#: uffd/user/views_user.py:30
 msgid "Users"
 msgstr "Accounts"
 
-#: uffd/user/views_user.py:51
+#: uffd/user/views_user.py:50
 msgid "Login name does not meet requirements"
 msgstr "Anmeldename entspricht nicht den Anforderungen"
 
-#: uffd/user/views_user.py:56
+#: uffd/user/views_user.py:55
 msgid "Mail is invalid"
 msgstr "E-Mail-Adresse nicht valide"
 
-#: uffd/user/views_user.py:60
+#: uffd/user/views_user.py:59
 msgid "Display name does not meet requirements"
 msgstr "Anzeigename entspricht nicht den Anforderungen"
 
-#: uffd/user/views_user.py:65
+#: uffd/user/views_user.py:64
 msgid "Password is invalid"
 msgstr "Passwort ist ungültig"
 
-#: uffd/user/views_user.py:79
+#: uffd/user/views_user.py:78
 msgid "Service user created"
 msgstr "Service-Account erstellt"
 
-#: uffd/user/views_user.py:82
+#: uffd/user/views_user.py:81
 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:84
+#: uffd/user/views_user.py:83
 msgid "User updated"
 msgstr "Account aktualisiert"
 
-#: uffd/user/views_user.py:95
+#: uffd/user/views_user.py:94
 msgid "Deleted user"
 msgstr "Account gelöscht"
 
diff --git a/uffd/user/views_group.py b/uffd/user/views_group.py
index 1526d10298c27d1b20d39d5ddf078be2f8f144a5..bfd71c2dee856c65f56edb354561184b831a4b5a 100644
--- a/uffd/user/views_group.py
+++ b/uffd/user/views_group.py
@@ -1,4 +1,4 @@
-from flask import Blueprint, render_template, current_app, request, abort
+from flask import Blueprint, render_template, current_app, request
 from flask_babel import lazy_gettext
 
 from uffd.navbar import register_navbar
@@ -7,15 +7,15 @@ from uffd.session import login_required
 from .models import Group
 
 bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/')
-@bp.before_request
-@login_required()
-def group_acl():
-	if not group_acl_check():
-		abort(403)
 
 def group_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
+@bp.before_request
+@login_required(group_acl_check)
+def group_acl():
+	pass
+
 @bp.route("/")
 @register_navbar(23, lazy_gettext('Groups'), icon='layer-group', blueprint=bp, visible=group_acl_check)
 def index():
diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py
index 2b4497dd2a963bfbe0c034563cd2fcec05b91f65..c98ba361889dbfc10807de5259b12c0d2257b9b0 100644
--- a/uffd/user/views_user.py
+++ b/uffd/user/views_user.py
@@ -1,7 +1,7 @@
 import csv
 import io
 
-from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
 from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
@@ -18,15 +18,14 @@ 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():
-	if not user_acl_check():
-		abort(403)
-
 def user_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
+@bp.before_request
+@login_required(user_acl_check)
+def user_acl():
+	pass
+
 @bp.route("/")
 @register_navbar(21, lazy_gettext('Users'), icon='users', blueprint=bp, visible=user_acl_check)
 def index():