diff --git a/tests/test_invite.py b/tests/test_invite.py
index 7c6bf2fb762c447d38d8fc5f0088ceca51c470df..f3d652ac7f5dc7ab66ab126a9947e0eded1bad20 100644
--- a/tests/test_invite.py
+++ b/tests/test_invite.py
@@ -326,8 +326,7 @@ class TestInviteAdminViews(UffdTestCase):
 		self.login_as('user')
 		r = self.client.get(path=url_for('invite.index'), follow_redirects=True)
 		dump('invite_index_noaccess', r)
-		self.assertEqual(r.status_code, 200)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 
 	def test_index_signupperm(self):
 		current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access'
@@ -345,7 +344,6 @@ class TestInviteAdminViews(UffdTestCase):
 		self.login_as('user')
 		r = self.client.get(path=url_for('invite.index'), follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
-		self.assertNotIn('Access denied'.encode(), r.data)
 		self.assertNotIn(token1.encode(), r.data)
 		self.assertIn(token2.encode(), r.data)
 		self.assertNotIn(token3.encode(), r.data)
@@ -362,7 +360,6 @@ class TestInviteAdminViews(UffdTestCase):
 		self.login_as('user')
 		r = self.client.get(path=url_for('invite.index'), follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
-		self.assertNotIn('Access denied'.encode(), r.data)
 		self.assertNotIn('testrole1'.encode(), r.data)
 		self.assertIn('testrole2'.encode(), r.data)
 
diff --git a/tests/test_role.py b/tests/test_role.py
index 6fef9547df589eca9aa3fc382bac471d9bdf7b11..0adbde99e98f640e50e689b53f22a7f7fa56eda7 100644
--- a/tests/test_role.py
+++ b/tests/test_role.py
@@ -251,6 +251,7 @@ class TestRoleViews(UffdTestCase):
 		ldap.session.commit()
 		role = Role(name='test')
 		db.session.add(role)
+		role.groups[self.get_admin_group()] = RoleGroup()
 		user1 = self.get_user()
 		user2 = self.get_admin()
 		service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))
@@ -281,26 +282,31 @@ class TestRoleViews(UffdTestCase):
 			ldap.session.delete(user)
 		ldap.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service'))
 		ldap.session.commit()
+		admin_role = Role(name='admin', is_default=True)
+		db.session.add(admin_role)
+		admin_role.groups[self.get_admin_group()] = RoleGroup()
 		role = Role(name='test', is_default=True)
 		db.session.add(role)
 		user1 = self.get_user()
 		user2 = self.get_admin()
 		service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))
 		role.members.add(service_user)
-		self.assertSetEqual(set(user1.roles_effective), {role})
-		self.assertSetEqual(set(user2.roles_effective), {role})
+		self.assertSetEqual(set(user1.roles_effective), {role, admin_role})
+		self.assertSetEqual(set(user2.roles_effective), {role, admin_role})
 		self.assertSetEqual(set(service_user.roles_effective), {role})
 		db.session.commit()
 		role_id = role.id
+		admin_role_id = admin_role.id
 		self.assertSetEqual(set(role.members), {service_user})
 		r = self.client.get(path=url_for('role.unset_default', roleid=role.id), follow_redirects=True)
 		dump('role_unset_default', r)
 		self.assertEqual(r.status_code, 200)
 		role = Role.query.get(role_id)
+		admin_role = Role.query.get(admin_role_id)
 		service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))
 		self.assertSetEqual(set(role.members), {service_user})
-		self.assertSetEqual(set(user1.roles_effective), set())
-		self.assertSetEqual(set(user2.roles_effective), set())
+		self.assertSetEqual(set(user1.roles_effective), {admin_role})
+		self.assertSetEqual(set(user2.roles_effective), {admin_role})
 		ldap.session.delete(service_user)
 		ldap.session.commit()
 
diff --git a/tests/test_rolemod.py b/tests/test_rolemod.py
index d94fce37bda958fddcf99165bb76ac1c4c17d65f..81a39c97c6ea0e398bdf75db1b68a79b1e503852 100644
--- a/tests/test_rolemod.py
+++ b/tests/test_rolemod.py
@@ -36,8 +36,7 @@ class TestRolemodViews(UffdTestCase):
 		db.session.commit()
 		r = self.client.get(path=url_for('rolemod.index'), follow_redirects=True)
 		dump('rolemod_acl_notmod', r)
-		self.assertEqual(r.status_code, 200)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 
 	def test_show(self):
 		role = Role(name='test', moderator_group=self.get_access_group())
@@ -64,7 +63,7 @@ class TestRolemodViews(UffdTestCase):
 		db.session.commit()
 		r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True)
 		dump('rolemod_show_noperm', r)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 
 	def test_show_nomod(self):
 		# Make sure we pass the blueprint-wide acl check
@@ -74,7 +73,7 @@ class TestRolemodViews(UffdTestCase):
 		db.session.commit()
 		r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True)
 		dump('rolemod_show_nomod', r)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 
 	def test_update(self):
 		role = Role(name='test', description='old_description', moderator_group=self.get_access_group())
@@ -102,7 +101,7 @@ class TestRolemodViews(UffdTestCase):
 		db.session.commit()
 		r = self.client.post(path=url_for('rolemod.update', role_id=role.id), data={'description': 'new_description'}, follow_redirects=True)
 		dump('rolemod_update_noperm', r)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 		self.assertEqual(Role.query.get(role.id).description, 'old_description')
 
 	def test_update_nomod(self):
@@ -113,7 +112,7 @@ class TestRolemodViews(UffdTestCase):
 		db.session.commit()
 		r = self.client.post(path=url_for('rolemod.update', role_id=role.id), data={'description': 'new_description'}, follow_redirects=True)
 		dump('rolemod_update_nomod', r)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 		self.assertEqual(Role.query.get(role.id).description, 'old_description')
 
 	def test_delete_member(self):
@@ -160,7 +159,7 @@ class TestRolemodViews(UffdTestCase):
 		self.assertTrue(user in role.members)
 		r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True)
 		dump('rolemod_delete_member_noperm', r)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 		user_updated = self.get_admin()
 		role = Role.query.get(role.id)
 		self.assertTrue(user_updated in role.members)
@@ -177,7 +176,7 @@ class TestRolemodViews(UffdTestCase):
 		self.assertTrue(user in role.members)
 		r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True)
 		dump('rolemod_delete_member_nomod', r)
-		self.assertIn('Access denied'.encode(), r.data)
+		self.assertEqual(r.status_code, 403)
 		user_updated = self.get_admin()
 		role = Role.query.get(role.id)
 		self.assertTrue(user_updated in role.members)
diff --git a/uffd/__init__.py b/uffd/__init__.py
index 1a9585691875ff4886f3d6a48e1fee5166907862..90feeafd1185b87bb1f38797da8f37aab62a8a82 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -2,12 +2,12 @@ import os
 import secrets
 import sys
 
-from flask import Flask, redirect, url_for, request
+from flask import Flask, redirect, url_for, request, render_template
 from flask_babel import Babel
 from werkzeug.routing import IntegerConverter
 from werkzeug.serving import make_ssl_devcert
 from werkzeug.contrib.profiler import ProfilerMiddleware
-from werkzeug.exceptions import InternalServerError
+from werkzeug.exceptions import InternalServerError, Forbidden
 from flask_migrate import Migrate
 
 import uffd.ldap
@@ -94,6 +94,10 @@ def create_app(test_config=None): # pylint: disable=too-many-locals
 		app.test_request_context().push() # LDAP ORM requires request context
 		return {'db': db, 'ldap': uffd.ldap.ldap, 'User': User, 'Group': Group, 'Role': Role, 'Mail': Mail}
 
+	@app.errorhandler(403)
+	def handle_403(error):
+		return render_template('403.html', description=error.description if error.description != Forbidden.description else None), 403
+
 	@app.route("/")
 	def index(): #pylint: disable=unused-variable
 		return redirect(url_for('selfservice.index'))
diff --git a/uffd/invite/views.py b/uffd/invite/views.py
index d93dd892bddd3f65ec1041215cba5cd841a4e8b1..65f4c51c0358e8c1f999c623b417ac34b638d7d7 100644
--- a/uffd/invite/views.py
+++ b/uffd/invite/views.py
@@ -1,7 +1,7 @@
 import datetime
 import functools
 
-from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
+from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, abort
 from flask_babel import gettext as _, lazy_gettext
 import sqlalchemy
 
@@ -35,8 +35,7 @@ def invite_acl_required(func):
 	@login_required()
 	def decorator(*args, **kwargs):
 		if not invite_acl():
-			flash('Access denied')
-			return redirect(url_for('index'))
+			abort(403)
 		return func(*args, **kwargs)
 	return decorator
 
diff --git a/uffd/mail/views.py b/uffd/mail/views.py
index 9cc7e585ed9f7cf0f9762bd8a2646eaa57c1f2f3..2e11905b0dce159ba1ee4c3d60329735c9817ce4 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
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
 from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
@@ -11,10 +11,9 @@ from uffd.mail.models import Mail
 bp = Blueprint("mail", __name__, template_folder='templates', url_prefix='/mail/')
 @bp.before_request
 @login_required()
-def mail_acl(): #pylint: disable=inconsistent-return-statements
+def mail_acl():
 	if not mail_acl_check():
-		flash('Access denied')
-		return redirect(url_for('index'))
+		abort(403)
 
 def mail_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py
index 287b4ab03d34d74e905a297428461f03a79490f3..69b5cc94a75144ca266c0f581094cac928723484 100644
--- a/uffd/mfa/views.py
+++ b/uffd/mfa/views.py
@@ -44,8 +44,7 @@ def admin_disable(uid):
 	# Group cannot be checked with login_required kwarg, because the config
 	# variable is not available when the decorator is processed
 	if not request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
-		flash('Access denied')
-		return redirect(url_for('index'))
+		abort(403)
 	user = User.query.filter_by(uid=uid).one()
 	MFAMethod.query.filter_by(dn=user.dn).delete()
 	db.session.commit()
diff --git a/uffd/oauth2/views.py b/uffd/oauth2/views.py
index 898d3d9be15130a4bee49a709e20cc82d62855cf..e330092945d9815254294e7ad9e57c2d24a4cd22 100644
--- a/uffd/oauth2/views.py
+++ b/uffd/oauth2/views.py
@@ -122,17 +122,11 @@ validator = UffdRequestValidator()
 server = oauthlib.oauth2.WebApplicationServer(validator)
 bp = Blueprint('oauth2', __name__, url_prefix='/oauth2/', template_folder='templates')
 
-def display_oauth_errors(func):
-	@functools.wraps(func)
-	def decorator(*args, **kwargs):
-		try:
-			return func(*args, **kwargs)
-		except oauthlib.oauth2.rfc6749.errors.OAuth2Error as ex:
-			return render_template('oauth2/error.html', error=type(ex).__name__, error_description=ex.description), 400
-	return decorator
+@bp.errorhandler(oauthlib.oauth2.rfc6749.errors.OAuth2Error)
+def handle_oauth2error(error):
+	return render_template('oauth2/error.html', error=type(error).__name__, error_description=error.description), 400
 
 @bp.route('/authorize', methods=['GET', 'POST'])
-@display_oauth_errors
 def authorize():
 	scopes, credentials = server.validate_authorization_request(request.url, request.method, request.form, request.headers)
 	client = OAuth2Client.from_id(credentials['client_id'])
@@ -177,7 +171,7 @@ def authorize():
 	# service access to his data. Since we only have trusted services (the
 	# clients defined in the server config), we don't ask for consent.
 	if not client.access_allowed(credentials['user']):
-		raise oauthlib.oauth2.rfc6749.errors.AccessDeniedError('User is not permitted to authenticate with this service.')
+		abort(403, description=_("You don't have the permission to access the service <b>%(service_name)s</b>.", service_name=client.client_id))
 	session['oauth2-clients'] = session.get('oauth2-clients', [])
 	if client.client_id not in session['oauth2-clients']:
 		session['oauth2-clients'].append(client.client_id)
diff --git a/uffd/role/views.py b/uffd/role/views.py
index 4c10a8b593f4cd9c03b78b1013d71f780a354d69..0b3e827f2a65d16cef7031fc98153d7769cb1a90 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
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
 from flask_babel import gettext as _, lazy_gettext
 import click
 
@@ -39,10 +39,9 @@ def add_cli_commands(state):
 
 @bp.before_request
 @login_required()
-def role_acl(): #pylint: disable=inconsistent-return-statements
+def role_acl():
 	if not role_acl_check():
-		flash(_('Access denied'))
-		return redirect(url_for('index'))
+		abort(403)
 
 def role_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
diff --git a/uffd/rolemod/views.py b/uffd/rolemod/views.py
index be60ee34207ddf864a379e621f4eac652d4ca941..5884910176a32e9a9f8f235d5fd5079c3ceb2cdc 100644
--- a/uffd/rolemod/views.py
+++ b/uffd/rolemod/views.py
@@ -1,4 +1,4 @@
-from flask import Blueprint, render_template, request, url_for, redirect, flash
+from flask import Blueprint, render_template, request, url_for, redirect, flash, abort
 from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
@@ -16,10 +16,9 @@ def user_is_rolemod():
 
 @bp.before_request
 @login_required()
-def acl_check(): #pylint: disable=inconsistent-return-statements
+def acl_check():
 	if not user_is_rolemod():
-		flash('Access denied')
-		return redirect(url_for('index'))
+		abort(403)
 
 @bp.route("/")
 @register_navbar(12, lazy_gettext('Moderation'), icon='user-lock', blueprint=bp, visible=user_is_rolemod)
@@ -33,8 +32,7 @@ def show(role_id):
 	User.query.all()
 	role = Role.query.get_or_404(role_id)
 	if role.moderator_group not in request.user.groups:
-		flash(_('Access denied'))
-		return redirect(url_for('index'))
+		abort(403)
 	return render_template('rolemod/show.html', role=role)
 
 @bp.route("/<int:role_id>", methods=['POST'])
@@ -42,8 +40,7 @@ def show(role_id):
 def update(role_id):
 	role = Role.query.get_or_404(role_id)
 	if role.moderator_group not in request.user.groups:
-		flash(_('Access denied'))
-		return redirect(url_for('index'))
+		abort(403)
 	if request.form['description'] != role.description:
 		if len(request.form['description']) > 256:
 			flash(_('Description too long'))
@@ -57,8 +54,7 @@ def update(role_id):
 def delete_member(role_id, member_dn):
 	role = Role.query.get_or_404(role_id)
 	if role.moderator_group not in request.user.groups:
-		flash(_('Access denied'))
-		return redirect(url_for('index'))
+		abort(403)
 	member = User.query.get_or_404(member_dn)
 	role.members.discard(member)
 	member.update_groups()
diff --git a/uffd/session/views.py b/uffd/session/views.py
index c674648b19a42631b1e9965c6d92c7f7f3cba35e..5e8433b038fbfbb746976c37f15ee8046f091e4b 100644
--- a/uffd/session/views.py
+++ b/uffd/session/views.py
@@ -129,8 +129,7 @@ def login_required(group=None):
 			if not request.user:
 				return redirect(url_for('mfa.auth', ref=request.full_path))
 			if not request.user.is_in_group(group):
-				flash(_('Access denied'))
-				return redirect(url_for('index'))
+				abort(403)
 			return func(*args, **kwargs)
 		return decorator
 	return wrapper
diff --git a/uffd/templates/403.html b/uffd/templates/403.html
new file mode 100644
index 0000000000000000000000000000000000000000..5a19dc9820bc1a6edabeb85c2856b48e4b4b5dcb
--- /dev/null
+++ b/uffd/templates/403.html
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<div class="row mt-2 justify-content-center">
+	<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
+		<div class="text-center">
+			<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
+		</div>
+		<div class="col-12">
+			<h2 class="text-center">{{ _('Access Denied') }}</h2>
+		</div>
+		<div class="form-group col-12">
+			<p>
+				{% if description %}
+				{{ description|safe }}
+				{% else %}
+				{{ _("You don't have the permission to access this page.") }}
+				{% endif %}
+			</p>
+		</div>
+	</div>
+</div>
+{% endblock %}
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index 8732d8a30f74045b143df675da5ec4687b98356f..89920cdc366a7daa31d3fb1afc9fceef4be3562d 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 89805c8fcbe12c13ec3e510b2da4d456802b979e..70e5f135a05d4ce7a2db7914eaf2ad24b9f2927e 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-04 21:18+0200\n"
+"POT-Creation-Date: 2021-09-04 21:53+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:56
+#: uffd/invite/views.py:55
 msgid "Invites"
 msgstr "Einladungslinks"
 
-#: uffd/invite/views.py:85
+#: uffd/invite/views.py:84
 msgid "The \"Expires After\" date is too far in the future"
 msgstr "Das Ablaufdatum liegt zu weit in der Zukunft"
 
-#: uffd/invite/views.py:88
+#: uffd/invite/views.py:87
 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:91
+#: uffd/invite/views.py:90
 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:119 uffd/invite/views.py:148
+#: uffd/invite/views.py:118 uffd/invite/views.py:147
 msgid "Invalid invite link"
 msgstr "Ungültiger Einladungslink"
 
-#: uffd/invite/views.py:136
+#: uffd/invite/views.py:135
 msgid "Roles successfully updated"
 msgstr "Rollen erfolgreich geändert"
 
-#: uffd/invite/views.py:151
+#: uffd/invite/views.py:150
 msgid "Invite link does not allow signup"
 msgstr "Einladungslink erlaubt keine Account-Registrierung"
 
-#: uffd/invite/views.py:173 uffd/selfservice/views.py:50
+#: uffd/invite/views.py:172 uffd/selfservice/views.py:50
 #: uffd/signup/views.py:49
 msgid "Passwords do not match"
 msgstr "Die Passwörter stimmen nicht überein"
 
-#: uffd/invite/views.py:178 uffd/signup/views.py:54
+#: uffd/invite/views.py:177 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:180 uffd/signup/views.py:56 uffd/signup/views.py:92
+#: uffd/invite/views.py:179 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:193 uffd/signup/views.py:68
+#: uffd/invite/views.py:192 uffd/signup/views.py:68
 msgid "Cound not send mail"
 msgstr "Mailversand fehlgeschlagen"
 
@@ -346,15 +346,15 @@ msgstr "Neuen Account registrieren"
 msgid "Login and add the roles to your account"
 msgstr "Anmelden und die Rollen zu deinem Account hinzufügen"
 
-#: uffd/mail/views.py:23
+#: uffd/mail/views.py:22
 msgid "Forwardings"
 msgstr "Weiterleitungen"
 
-#: uffd/mail/views.py:47
+#: uffd/mail/views.py:46
 msgid "Mail mapping updated."
 msgstr "Mailweiterleitung geändert."
 
-#: uffd/mail/views.py:56
+#: uffd/mail/views.py:55
 msgid "Deleted mail mapping."
 msgstr "Mailweiterleitung gelöscht."
 
@@ -383,19 +383,19 @@ msgstr "Speichern"
 msgid "Delete"
 msgstr "Löschen"
 
-#: uffd/mfa/views.py:54
+#: uffd/mfa/views.py:53
 msgid "Two-factor authentication was reset"
 msgstr "Zwei-Faktor-Authentifizierung wurde zurückgesetzt"
 
-#: uffd/mfa/views.py:83
+#: uffd/mfa/views.py:82
 msgid "Generate recovery codes first!"
 msgstr "Generiere zuerst die Wiederherstellungscodes!"
 
-#: uffd/mfa/views.py:92
+#: uffd/mfa/views.py:91
 msgid "Code is invalid"
 msgstr "Wiederherstellungscode ist ungültig"
 
-#: uffd/mfa/views.py:116
+#: uffd/mfa/views.py:115
 #, python-format
 msgid ""
 "2FA WebAuthn support disabled because import of the fido2 module failed "
@@ -404,16 +404,16 @@ msgstr ""
 "2FA WebAuthn Unterstützung deaktiviert, da das fido2 Modul nicht geladen "
 "werden konnte (%s)"
 
-#: uffd/mfa/views.py:225
+#: uffd/mfa/views.py:224
 #, python-format
 msgid "We received too many invalid attempts! Please wait at least %s."
 msgstr "Wir haben zu viele fehlgeschlagene Versuche! Bitte warte mindestens %s."
 
-#: uffd/mfa/views.py:239
+#: uffd/mfa/views.py:238
 msgid "You have exhausted your recovery codes. Please generate new ones now!"
 msgstr "Du hast keine Wiederherstellungscode mehr. Bitte generiere diese jetzt!"
 
-#: uffd/mfa/views.py:242
+#: uffd/mfa/views.py:241
 msgid ""
 "You only have a few recovery codes remaining. Make sure to generate new "
 "ones before they run out."
@@ -421,7 +421,7 @@ msgstr ""
 "Du hast nur noch wenige Wiederherstellungscodes übrig. Bitte generiere "
 "diese erneut bevor keine mehr übrig sind."
 
-#: uffd/mfa/views.py:246
+#: uffd/mfa/views.py:245
 msgid "Two-factor authentication failed"
 msgstr "Zwei-Faktor-Authentifizierung fehlgeschlagen"
 
@@ -704,7 +704,7 @@ 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:146 uffd/selfservice/views.py:76
+#: uffd/oauth2/views.py:140 uffd/selfservice/views.py:76
 #: uffd/session/views.py:95
 #, python-format
 msgid ""
@@ -714,14 +714,23 @@ msgstr ""
 "Wir haben zu viele Anfragen von deiner IP-Adresses bzw. aus deinem "
 "Netzwerk empfangen! Bitte warte mindestens %(delay)s."
 
-#: uffd/oauth2/views.py:154
+#: uffd/oauth2/views.py:148
 msgid "Device login is currently not available. Try again later!"
 msgstr "Geräte-Login ist gerade nicht verfügbar. Versuche es später nochmal!"
 
-#: uffd/oauth2/views.py:167
+#: uffd/oauth2/views.py:161
 msgid "Device login failed"
 msgstr "Gerätelogin fehlgeschlagen"
 
+#: uffd/oauth2/views.py:174
+#, python-format
+msgid ""
+"You don't have the permission to access the service "
+"<b>%(service_name)s</b>."
+msgstr ""
+"Du bist nicht berechtigt, auf den Dienst <b>%(service_name)s</b> "
+"zuzugreifen."
+
 #: uffd/oauth2/templates/oauth2/logout.html:10 uffd/templates/base.html:99
 msgid "Logout"
 msgstr "Abmelden"
@@ -764,19 +773,13 @@ msgstr ""
 "Automatisches Abmelden bei einigen Diensten fehlgeschlagen. Nochmal "
 "versuchen?"
 
-#: uffd/role/views.py:44 uffd/rolemod/views.py:36 uffd/rolemod/views.py:45
-#: uffd/rolemod/views.py:60 uffd/session/views.py:132
-#: 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:86
+#: uffd/role/views.py:50 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:101
+#: uffd/role/views.py:100
 msgid "Locked roles cannot be deleted"
 msgstr "Gesperrte Rollen können nicht gelöscht werden"
 
@@ -868,15 +871,15 @@ msgstr "derzeit enthaltene Gruppen"
 msgid "2FA required"
 msgstr "2FA erforderlich"
 
-#: uffd/rolemod/views.py:25
+#: uffd/rolemod/views.py:24
 msgid "Moderation"
 msgstr "Moderation"
 
-#: uffd/rolemod/views.py:49
+#: uffd/rolemod/views.py:46
 msgid "Description too long"
 msgstr "Beschreibung zu lang"
 
-#: uffd/rolemod/views.py:67
+#: uffd/rolemod/views.py:63
 msgid "Member removed"
 msgstr "Mitglied entfernt"
 
@@ -1192,15 +1195,15 @@ msgstr "Du hast keinen Zugriff auf diesen Service"
 msgid "You need to login first"
 msgstr "Du musst dich erst anmelden"
 
-#: uffd/session/views.py:149 uffd/session/views.py:159
+#: uffd/session/views.py:148 uffd/session/views.py:158
 msgid "Initiation code is no longer valid"
 msgstr "Startcode ist nicht mehr gültig"
 
-#: uffd/session/views.py:163
+#: uffd/session/views.py:162
 msgid "Invalid confirmation code"
 msgstr "Ungültiger Bestätigungscode"
 
-#: uffd/session/views.py:175 uffd/session/views.py:186
+#: uffd/session/views.py:174 uffd/session/views.py:185
 msgid "Invalid initiation code"
 msgstr "Ungültiger Startcode"
 
@@ -1403,6 +1406,14 @@ msgstr ""
 " Gründen keine Bestätigungsmail erhalten hast, kannst du den Prozess "
 "einfach von Vorne beginnen."
 
+#: uffd/templates/403.html:10
+msgid "Access Denied"
+msgstr "Zugriff verweigert"
+
+#: uffd/templates/403.html:17
+msgid "You don't have the permission to access this page."
+msgstr "Du bist nicht berechtigt, auf diese Seite zuzugreifen."
+
 #: uffd/templates/base.html:84
 msgid "Change"
 msgstr "Ändern"
@@ -1422,45 +1433,45 @@ msgstr ""
 "und manche Symbole (<code>%(symbols)s</code>), keine Umlaute. Bitte "
 "verwende einen Passwort-Manager."
 
-#: uffd/user/views_group.py:21
+#: uffd/user/views_group.py:20
 msgid "Groups"
 msgstr "Gruppen"
 
-#: uffd/user/views_user.py:32
+#: uffd/user/views_user.py:31
 msgid "Users"
 msgstr "Accounts"
 
-#: uffd/user/views_user.py:52
+#: uffd/user/views_user.py:51
 msgid "Login name does not meet requirements"
 msgstr "Anmeldename entspricht nicht den Anforderungen"
 
-#: uffd/user/views_user.py:57
+#: uffd/user/views_user.py:56
 msgid "Mail is invalid"
 msgstr "E-Mail-Adresse nicht valide"
 
-#: uffd/user/views_user.py:61
+#: uffd/user/views_user.py:60
 msgid "Display name does not meet requirements"
 msgstr "Anzeigename entspricht nicht den Anforderungen"
 
-#: uffd/user/views_user.py:66
+#: uffd/user/views_user.py:65
 msgid "Password is invalid"
 msgstr "Passwort ist ungültig"
 
-#: uffd/user/views_user.py:80
+#: uffd/user/views_user.py:79
 msgid "Service user created"
 msgstr "Service-Account erstellt"
 
-#: uffd/user/views_user.py:83
+#: uffd/user/views_user.py:82
 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:85
+#: uffd/user/views_user.py:84
 msgid "User updated"
 msgstr "Account aktualisiert"
 
-#: uffd/user/views_user.py:96
+#: uffd/user/views_user.py:95
 msgid "Deleted user"
 msgstr "Account gelöscht"
 
diff --git a/uffd/user/views_group.py b/uffd/user/views_group.py
index 480a7b59bbccac2b449249b7d7d23faaccba5734..1526d10298c27d1b20d39d5ddf078be2f8f144a5 100644
--- a/uffd/user/views_group.py
+++ b/uffd/user/views_group.py
@@ -1,5 +1,5 @@
-from flask import Blueprint, render_template, url_for, redirect, flash, current_app, request
-from flask_babel import gettext as _, lazy_gettext
+from flask import Blueprint, render_template, current_app, request, abort
+from flask_babel import lazy_gettext
 
 from uffd.navbar import register_navbar
 from uffd.session import login_required
@@ -9,10 +9,9 @@ from .models import Group
 bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/')
 @bp.before_request
 @login_required()
-def group_acl(): #pylint: disable=inconsistent-return-statements
+def group_acl():
 	if not group_acl_check():
-		flash(_('Access denied'))
-		return redirect(url_for('index'))
+		abort(403)
 
 def group_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py
index 2b2739465cc9e376afb73bc57d34e1fde790caa0..2b4497dd2a963bfbe0c034563cd2fcec05b91f65 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
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort
 from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
@@ -20,10 +20,9 @@ bp.add_app_template_global(User, 'User')
 
 @bp.before_request
 @login_required()
-def user_acl(): #pylint: disable=inconsistent-return-statements
+def user_acl():
 	if not user_acl_check():
-		flash(_('Access denied'))
-		return redirect(url_for('index'))
+		abort(403)
 
 def user_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])