diff --git a/tests/commands/test_user.py b/tests/commands/test_user.py
index 3612fc3ba5ec215443a4ca5de225129080cfa635..3fcf49e85a75765e4260d26348571bd0564e1649 100644
--- a/tests/commands/test_user.py
+++ b/tests/commands/test_user.py
@@ -57,6 +57,13 @@ class TestUserCLI(UffdTestCase):
 			self.assertTrue(user.password.verify('newpassword'))
 			self.assertEqual(user.roles, Role.query.filter_by(name='admin').all())
 			self.assertIn(self.get_admin_group(), user.groups)
+			self.assertFalse(user.is_deactivated)
+
+		result = self.app.test_cli_runner().invoke(args=['user', 'create', 'newuser2', '--mail', 'newmail2@example.com', '--deactivate'])
+		self.assertEqual(result.exit_code, 0)
+		with self.app.test_request_context():
+			user = User.query.filter_by(loginname='newuser2').first()
+			self.assertTrue(user.is_deactivated)
 
 	def test_update(self):
 		result = self.app.test_cli_runner().invoke(args=['user', 'update', 'doesnotexist', '--displayname', 'foo'])
@@ -106,6 +113,16 @@ class TestUserCLI(UffdTestCase):
 			user = User.query.filter_by(loginname='testuser').first()
 			self.assertEqual(user.roles, Role.query.filter_by(name='admin').all())
 			self.assertIn(self.get_admin_group(), user.groups)
+		result = self.app.test_cli_runner().invoke(args=['user', 'update', 'testuser', '--deactivate'])
+		self.assertEqual(result.exit_code, 0)
+		with self.app.test_request_context():
+			user = User.query.filter_by(loginname='testuser').first()
+			self.assertTrue(user.is_deactivated)
+		result = self.app.test_cli_runner().invoke(args=['user', 'update', 'testuser', '--activate'])
+		self.assertEqual(result.exit_code, 0)
+		with self.app.test_request_context():
+			user = User.query.filter_by(loginname='testuser').first()
+			self.assertFalse(user.is_deactivated)
 
 	def test_delete(self):
 		with self.app.test_request_context():
diff --git a/tests/models/test_invite.py b/tests/models/test_invite.py
index fbbf8ab7896fc41e0b54f7825346a3657053a9fe..31fb104a4063a51c3100c2469380e6648ec9854f 100644
--- a/tests/models/test_invite.py
+++ b/tests/models/test_invite.py
@@ -38,6 +38,9 @@ class TestInviteModel(UffdTestCase):
 		invite.creator = self.get_admin()
 		self.assertTrue(invite.permitted)
 		self.assertTrue(invite.active)
+		invite.creator.is_deactivated = True
+		self.assertFalse(invite.permitted)
+		self.assertFalse(invite.active)
 		invite.creator = self.get_user()
 		self.assertFalse(invite.permitted)
 		self.assertFalse(invite.active)
diff --git a/tests/views/test_api.py b/tests/views/test_api.py
index fce8554b4095cbe8ab1779bb59b04cf956fa7d9f..996bb1be9b839d6c730338182e02a1bcb7c7a9d2 100644
--- a/tests/views/test_api.py
+++ b/tests/views/test_api.py
@@ -109,6 +109,13 @@ class TestAPICheckPassword(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.json, None)
 
+	def test_deactivated(self):
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.post(path=url_for('api.checkpassword'), data={'loginname': 'testuser', 'password': 'userpassword'}, headers=[basic_auth('test', 'test')])
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(r.json, None)
+
 class TestAPIGetusers(UffdTestCase):
 	def setUpDB(self):
 		db.session.add(APIClient(service=Service(name='test'), auth_username='test', auth_password='test', perm_users=True))
@@ -201,6 +208,23 @@ class TestAPIGetusers(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.json, [])
 
+	def test_deactivated(self):
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.get(path=url_for('api.getusers'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(self.fix_result(r.json), [
+			{'displayname': 'Test User', 'email': 'test@example.com', 'id': 10000, 'loginname': 'testuser', 'groups': ['uffd_access', 'users']},
+			{'displayname': 'Test Admin', 'email': 'admin@example.com', 'id': 10001, 'loginname': 'testadmin', 'groups': ['uffd_access', 'uffd_admin', 'users']}
+		])
+		Service.query.filter_by(name='test').first().hide_deactivated_users = True
+		db.session.commit()
+		r = self.client.get(path=url_for('api.getusers'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(self.fix_result(r.json), [
+			{'displayname': 'Test Admin', 'email': 'admin@example.com', 'id': 10001, 'loginname': 'testadmin', 'groups': ['uffd_access', 'uffd_admin', 'users']}
+		])
+
 class TestAPIGetgroups(UffdTestCase):
 	def setUpDB(self):
 		db.session.add(APIClient(service=Service(name='test'), auth_username='test', auth_password='test', perm_users=True))
@@ -220,6 +244,26 @@ class TestAPIGetgroups(UffdTestCase):
 			{'id': 20003, 'members': ['testadmin'], 'name': 'uffd_admin'}
 		])
 
+	def test_all_deactivated_members(self):
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.get(path=url_for('api.getgroups'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(self.fix_result(r.json), [
+			{'id': 20001, 'members': ['testadmin', 'testuser'], 'name': 'users'},
+			{'id': 20002, 'members': ['testadmin', 'testuser'], 'name': 'uffd_access'},
+			{'id': 20003, 'members': ['testadmin'], 'name': 'uffd_admin'}
+		])
+		Service.query.filter_by(name='test').first().hide_deactivated_users = True
+		db.session.commit()
+		r = self.client.get(path=url_for('api.getgroups'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(self.fix_result(r.json), [
+			{'id': 20001, 'members': ['testadmin'], 'name': 'users'},
+			{'id': 20002, 'members': ['testadmin'], 'name': 'uffd_access'},
+			{'id': 20003, 'members': ['testadmin'], 'name': 'uffd_admin'}
+		])
+
 	def test_id(self):
 		r = self.client.get(path=url_for('api.getgroups', id=20002), headers=[basic_auth('test', 'test')], follow_redirects=True)
 		self.assertEqual(r.status_code, 200)
@@ -257,6 +301,21 @@ class TestAPIGetgroups(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.json, [])
 
+	def test_member_deactivated(self):
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.get(path=url_for('api.getgroups', member='testuser'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(self.fix_result(r.json), [
+			{'id': 20001, 'members': ['testadmin', 'testuser'], 'name': 'users'},
+			{'id': 20002, 'members': ['testadmin', 'testuser'], 'name': 'uffd_access'},
+		])
+		Service.query.filter_by(name='test').first().hide_deactivated_users = True
+		db.session.commit()
+		r = self.client.get(path=url_for('api.getgroups', member='testuser'), headers=[basic_auth('test', 'test')], follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertEqual(self.fix_result(r.json), [])
+
 class TestAPIRemailerResolve(UffdTestCase):
 	def setUpDB(self):
 		db.session.add(APIClient(service=Service(name='test'), auth_username='test', auth_password='test', perm_remailer=True))
diff --git a/tests/views/test_oauth2.py b/tests/views/test_oauth2.py
index 72c9ff7f6d2b7a0cc0e898109dc46fe988fd3a3e..6d26baf6b5f1f910df17cbe9a04a3011eae1a169 100644
--- a/tests/views/test_oauth2.py
+++ b/tests/views/test_oauth2.py
@@ -139,6 +139,23 @@ class TestViews(UffdTestCase):
 		r = self.client.post(path=url_for('session.devicelogin_submit', ref=ref), data={'confirmation-code': code}, follow_redirects=False)
 		self.assert_authorization(r)
 
+	def test_authorization_devicelogin_auth_deactivated(self):
+		with self.client.session_transaction() as _session:
+			initiation = OAuth2DeviceLoginInitiation(client=OAuth2Client.query.filter_by(client_id='test').one())
+			db.session.add(initiation)
+			confirmation = DeviceLoginConfirmation(initiation=initiation, user=self.get_user())
+			db.session.add(confirmation)
+			db.session.commit()
+			_session['devicelogin_id'] = initiation.id
+			_session['devicelogin_secret'] = initiation.secret
+			code = confirmation.code
+		self.client.get(path='/')
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		ref = url_for('oauth2.authorize', response_type='code', client_id='test', state='teststate', redirect_uri='http://localhost:5009/callback')
+		r = self.client.post(path=url_for('session.devicelogin_submit', ref=ref), data={'confirmation-code': code}, follow_redirects=True)
+		self.assertIn(b'Device login failed', r.data)
+
 	def get_auth_code(self):
 		self.login_as('user')
 		r = self.client.get(path=url_for('oauth2.authorize', response_type='code', client_id='test', state='teststate', redirect_uri='http://localhost:5009/callback', scope='profile'), follow_redirects=False)
@@ -195,7 +212,25 @@ class TestViews(UffdTestCase):
 		self.assertEqual(r.content_type, 'application/json')
 		self.assertEqual(r.json['error'], 'unsupported_grant_type')
 
+	def test_token_deactivated_user(self):
+		code = self.get_auth_code()
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.post(path=url_for('oauth2.token'),
+			data={'grant_type': 'authorization_code', 'code': code, 'redirect_uri': 'http://localhost:5009/callback', 'client_id': 'test', 'client_secret': 'testsecret'}, follow_redirects=True)
+		self.assertIn(r.status_code, [400, 401]) # oauthlib behaviour changed between v2.1.0 and v3.1.0
+		self.assertEqual(r.content_type, 'application/json')
+
 	def test_userinfo_invalid_access_token(self):
 		token = 'invalidtoken'
 		r = self.client.get(path=url_for('oauth2.userinfo'), headers=[('Authorization', 'Bearer %s'%token)], follow_redirects=True)
 		self.assertEqual(r.status_code, 401)
+
+	def test_userinfo_invalid_access_token(self):
+		r = self.client.post(path=url_for('oauth2.token'),
+			data={'grant_type': 'authorization_code', 'code': self.get_auth_code(), 'redirect_uri': 'http://localhost:5009/callback', 'client_id': 'test', 'client_secret': 'testsecret'}, follow_redirects=True)
+		token = r.json['access_token']
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.get(path=url_for('oauth2.userinfo'), headers=[('Authorization', 'Bearer %s'%token)], follow_redirects=True)
+		self.assertEqual(r.status_code, 401)
diff --git a/tests/views/test_selfservice.py b/tests/views/test_selfservice.py
index 1b598aa92ac57e06b9be460e01821e89a40868f5..b5b202414faa3ba2be6825e21e74574c5b2544ee 100644
--- a/tests/views/test_selfservice.py
+++ b/tests/views/test_selfservice.py
@@ -396,6 +396,19 @@ class TestSelfservice(UffdTestCase):
 		self.assertFalse(hasattr(self.app, 'last_mail'))
 		self.assertEqual(len(PasswordToken.query.all()), 0)
 
+	def test_forgot_password_wrong_user(self):
+		user = self.get_user()
+		r = self.client.get(path=url_for('selfservice.forgot_password'))
+		self.assertEqual(r.status_code, 200)
+		user = self.get_user()
+		user.is_deactivated = True
+		db.session.commit()
+		r = self.client.post(path=url_for('selfservice.forgot_password'),
+			data={'loginname': user.loginname, 'mail': user.primary_email.address}, follow_redirects=True)
+		self.assertEqual(r.status_code, 200)
+		self.assertFalse(hasattr(self.app, 'last_mail'))
+		self.assertEqual(len(PasswordToken.query.all()), 0)
+
 	def test_token_password(self):
 		user = self.get_user()
 		token = PasswordToken(user=user)
diff --git a/tests/views/test_services.py b/tests/views/test_services.py
index a40e447f07702651163a20429bf80d2ef20a082c..51e296a4820fe7a9c52f4d98f5ee2d1cf0fc565c 100644
--- a/tests/views/test_services.py
+++ b/tests/views/test_services.py
@@ -172,6 +172,7 @@ class TestServiceAdminViews(UffdTestCase):
 		self.assertEqual(service.access_group, None)
 		self.assertEqual(service.remailer_mode, RemailerMode.DISABLED)
 		self.assertEqual(service.enable_email_preferences, False)
+		self.assertEqual(service.hide_deactivated_users, False)
 
 	def test_edit_access_all(self):
 		self.login_as('admin')
@@ -209,6 +210,24 @@ class TestServiceAdminViews(UffdTestCase):
 		self.assertEqual(service.limit_access, True)
 		self.assertEqual(service.access_group, self.get_users_group())
 
+	def test_edit_hide_deactivated_users(self):
+		self.login_as('admin')
+		r = self.client.post(
+			path=url_for('service.edit_submit', id=self.service_id),
+			follow_redirects=True,
+			data={
+				'name': 'test1',
+				'access-group': '',
+				'remailer-mode': 'DISABLED',
+				'remailer-overwrite-mode': 'ENABLED_V2',
+				'remailer-overwrite-users': '',
+				'hide_deactivated_users': '1',
+			},
+		)
+		self.assertEqual(r.status_code, 200)
+		service = Service.query.get(self.service_id)
+		self.assertEqual(service.hide_deactivated_users, True)
+
 	def test_edit_email_preferences(self):
 		self.login_as('admin')
 		r = self.client.post(
diff --git a/tests/views/test_session.py b/tests/views/test_session.py
index 0cb5c340688eed0a6d2eab8b2f67453f6e2268eb..1780ac87fa4ed8e49f61e5ae597d25492968d948 100644
--- a/tests/views/test_session.py
+++ b/tests/views/test_session.py
@@ -17,7 +17,7 @@ class TestSession(UffdTestCase):
 		@self.app.route('/test_login_required')
 		@login_required()
 		def test_login_required():
-			return 'SUCCESS', 200
+			return 'SUCCESS ' + request.user.loginname, 200
 
 		@self.app.route('/test_group_required1')
 		@login_required(lambda: request.user.is_in_group('users'))
@@ -38,15 +38,10 @@ class TestSession(UffdTestCase):
 		self.assertIsNotNone(request.user)
 
 	def assertLoggedIn(self):
-		self.assertIsNotNone(request.user)
-		self.assertEqual(self.client.get(path=url_for('test_login_required'), follow_redirects=True).data, b'SUCCESS')
-		self.assertEqual(request.user.loginname, self.get_user().loginname)
+		self.assertEqual(self.client.get(path=url_for('test_login_required'), follow_redirects=True).data, b'SUCCESS testuser')
 
 	def assertLoggedOut(self):
-		self.assertIsNone(request.user)
-		self.assertNotEqual(self.client.get(path=url_for('test_login_required'),
-							follow_redirects=True).data, b'SUCCESS')
-		self.assertEqual(request.user, None)
+		self.assertNotIn(b'SUCCESS', self.client.get(path=url_for('test_login_required'), follow_redirects=True).data)
 
 	def test_login(self):
 		self.assertLoggedOut()
@@ -78,7 +73,7 @@ class TestSession(UffdTestCase):
 	def test_redirect(self):
 		r = self.login_as('user', ref=url_for('test_login_required'))
 		self.assertEqual(r.status_code, 200)
-		self.assertEqual(r.data, b'SUCCESS')
+		self.assertEqual(r.data, b'SUCCESS testuser')
 
 	def test_wrong_password(self):
 		r = self.client.post(path=url_for('session.login'),
@@ -125,6 +120,20 @@ class TestSession(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertLoggedOut()
 
+	def test_deactivated(self):
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.login_as('user')
+		dump('login_deactivated', r)
+		self.assertEqual(r.status_code, 200)
+		self.assertLoggedOut()
+
+	def test_deactivated_after_login(self):
+		self.login_as('user')
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		self.assertLoggedOut()
+
 	def test_group_required(self):
 		self.login()
 		self.assertEqual(self.client.get(path=url_for('test_group_required1'),
diff --git a/tests/views/test_signup.py b/tests/views/test_signup.py
index 72f80488fb5ad6b58662f330ab66ca8c8a331a40..7db3b7fe7b7f4a91cbbf55827e8e332ab769acaa 100644
--- a/tests/views/test_signup.py
+++ b/tests/views/test_signup.py
@@ -3,8 +3,7 @@ import datetime
 from flask import url_for, request
 
 from uffd.database import db
-from uffd.models import Signup, Role, RoleGroup, FeatureFlag
-from uffd.views.session import login_get_user
+from uffd.models import User, Signup, Role, RoleGroup, FeatureFlag
 
 from tests.utils import dump, UffdTestCase, db_flush
 
@@ -161,13 +160,13 @@ class TestSignupViews(UffdTestCase):
 		signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
 		signup = refetch_signup(signup)
 		self.assertFalse(signup.completed)
-		self.assertIsNone(login_get_user('newuser', 'notsecret'))
+		self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
 		r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True)
 		dump('test_signup_confirm', r)
 		self.assertEqual(r.status_code, 200)
 		signup = refetch_signup(signup)
 		self.assertFalse(signup.completed)
-		self.assertIsNone(login_get_user('newuser', 'notsecret'))
+		self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
 		r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
 		dump('test_signup_confirm_submit', r)
 		self.assertEqual(r.status_code, 200)
@@ -176,7 +175,7 @@ class TestSignupViews(UffdTestCase):
 		self.assertEqual(signup.user.loginname, 'newuser')
 		self.assertEqual(signup.user.displayname, 'New User')
 		self.assertEqual(signup.user.primary_email.address, 'new@example.com')
-		self.assertIsNotNone(login_get_user('newuser', 'notsecret'))
+		self.assertTrue(User.query.filter_by(loginname='newuser').one_or_none().password.verify('notsecret'))
 
 	def test_confirm_loggedin(self):
 		baserole = Role(name='baserole', is_default=True)
diff --git a/tests/views/test_user.py b/tests/views/test_user.py
index a024b53d6c79322c427108feda3953be778af98c..6bffa84d0dbae6b62799d492546fc0f4367bc167 100644
--- a/tests/views/test_user.py
+++ b/tests/views/test_user.py
@@ -353,12 +353,31 @@ class TestUserViews(UffdTestCase):
 		dump('user_show', r)
 		self.assertEqual(r.status_code, 200)
 
+	def test_show_self(self):
+		r = self.client.get(path=url_for('user.show', id=self.get_admin().id), follow_redirects=True)
+		dump('user_show_self', r)
+		self.assertEqual(r.status_code, 200)
+
 	def test_delete(self):
 		r = self.client.get(path=url_for('user.delete', id=self.get_user().id), follow_redirects=True)
 		dump('user_delete', r)
 		self.assertEqual(r.status_code, 200)
 		self.assertIsNone(self.get_user())
 
+	def test_deactivate(self):
+		r = self.client.get(path=url_for('user.deactivate', id=self.get_user().id), follow_redirects=True)
+		dump('user_deactivate', r)
+		self.assertEqual(r.status_code, 200)
+		self.assertTrue(self.get_user().is_deactivated)
+
+	def test_activate(self):
+		self.get_user().is_deactivated = True
+		db.session.commit()
+		r = self.client.get(path=url_for('user.activate', id=self.get_user().id), follow_redirects=True)
+		dump('user_activate', r)
+		self.assertEqual(r.status_code, 200)
+		self.assertFalse(self.get_user().is_deactivated)
+
 	def test_csvimport(self):
 		role1 = Role(name='role1')
 		db.session.add(role1)
diff --git a/uffd/commands/user.py b/uffd/commands/user.py
index 2db24b3fe83cacda6207d4b538ff5f717561b88b..68aa675faa3588dcc2c6b65fa536b0b7a9b4fa39 100644
--- a/uffd/commands/user.py
+++ b/uffd/commands/user.py
@@ -12,7 +12,7 @@ user_command = AppGroup('user', help='Manage users')
 
 def update_attrs(user, mail=None, displayname=None, password=None,
                  prompt_password=False, clear_roles=False,
-                 add_role=tuple(), remove_role=tuple()):
+                 add_role=tuple(), remove_role=tuple(), deactivate=None):
 	if password is None and prompt_password:
 		password = click.prompt('Password', hide_input=True, confirmation_prompt='Confirm password')
 	if mail is not None and not user.set_primary_email_address(mail):
@@ -21,6 +21,8 @@ def update_attrs(user, mail=None, displayname=None, password=None,
 		raise click.ClickException('Invalid displayname')
 	if password is not None and not user.set_password(password):
 		raise click.ClickException('Invalid password')
+	if deactivate is not None:
+		user.is_deactivated = deactivate
 	if clear_roles:
 		user.roles.clear()
 	for role_name in add_role:
@@ -49,6 +51,7 @@ def show(loginname):
 		if user is None:
 			raise click.ClickException(f'User {loginname} not found')
 		click.echo(f'Loginname: {user.loginname}')
+		click.echo(f'Deactivated: {user.is_deactivated}')
 		click.echo(f'Displayname: {user.displayname}')
 		click.echo(f'Mail: {user.primary_email.address}')
 		click.echo(f'Service User: {user.is_service_user}')
@@ -65,7 +68,8 @@ def show(loginname):
 @click.option('--password', help='Password for SSO login. Login disabled if unset.')
 @click.option('--prompt-password', is_flag=True, flag_value=True, default=False, help='Read password interactively from terminal.')
 @click.option('--add-role', multiple=True, help='Add role to user. Repeat to add multiple roles.', metavar='ROLE_NAME')
-def create(loginname, mail, displayname, service, password, prompt_password, add_role):
+@click.option('--deactivate', is_flag=True, flag_value=True, default=None, help='Deactivate account.')
+def create(loginname, mail, displayname, service, password, prompt_password, add_role, deactivate):
 	with current_app.test_request_context():
 		if displayname is None:
 			displayname = loginname
@@ -74,7 +78,7 @@ def create(loginname, mail, displayname, service, password, prompt_password, add
 			raise click.ClickException('Invalid loginname')
 		try:
 			db.session.add(user)
-			update_attrs(user, mail, displayname, password, prompt_password, add_role=add_role)
+			update_attrs(user, mail, displayname, password, prompt_password, add_role=add_role, deactivate=deactivate)
 			db.session.commit()
 		except IntegrityError:
 			# pylint: disable=raise-missing-from
@@ -89,13 +93,14 @@ def create(loginname, mail, displayname, service, password, prompt_password, add
 @click.option('--clear-roles', is_flag=True, flag_value=True, default=False, help='Remove all roles from user. Executed before --add-role.')
 @click.option('--add-role', multiple=True, help='Add role to user. Repeat to add multiple roles.')
 @click.option('--remove-role', multiple=True, help='Remove role from user. Repeat to remove multiple roles.')
-def update(loginname, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role):
+@click.option('--deactivate/--activate', default=None, help='Deactivate or reactivate account.')
+def update(loginname, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role, deactivate):
 	with current_app.test_request_context():
 		user = User.query.filter_by(loginname=loginname).one_or_none()
 		if user is None:
 			raise click.ClickException(f'User {loginname} not found')
 		try:
-			update_attrs(user, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role)
+			update_attrs(user, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role, deactivate)
 			db.session.commit()
 		except IntegrityError:
 			# pylint: disable=raise-missing-from
diff --git a/uffd/migrations/versions/23293f32b503_deactivate_users.py b/uffd/migrations/versions/23293f32b503_deactivate_users.py
new file mode 100644
index 0000000000000000000000000000000000000000..c25dbf6cbc1045a0f398f0a2730da096c466806e
--- /dev/null
+++ b/uffd/migrations/versions/23293f32b503_deactivate_users.py
@@ -0,0 +1,90 @@
+"""Deactivate users
+
+Revision ID: 23293f32b503
+Revises: e249233e2a31
+Create Date: 2022-11-10 02:06:27.766520
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = '23293f32b503'
+down_revision = 'e249233e2a31'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+	meta = sa.MetaData(bind=op.get_bind())
+	with op.batch_alter_table('service', schema=None) as batch_op:
+		batch_op.add_column(sa.Column('hide_deactivated_users', sa.Boolean(), nullable=False, server_default=sa.false()))
+	service = sa.Table('service', meta,
+		sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+		sa.Column('name', sa.String(length=255), nullable=False),
+		sa.Column('limit_access', sa.Boolean(), nullable=False),
+		sa.Column('access_group_id', sa.Integer(), nullable=True),
+		sa.Column('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), nullable=False),
+		sa.Column('enable_email_preferences', sa.Boolean(), nullable=False),
+		sa.Column('hide_deactivated_users', sa.Boolean(), nullable=False, server_default=sa.false()),
+		sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'),
+		sa.PrimaryKeyConstraint('id', name=op.f('pk_service')),
+		sa.UniqueConstraint('name', name=op.f('uq_service_name'))
+	)
+	with op.batch_alter_table('service', copy_from=service) as batch_op:
+		batch_op.alter_column('hide_deactivated_users', server_default=None)
+	with op.batch_alter_table('user', schema=None) as batch_op:
+		batch_op.add_column(sa.Column('is_deactivated', sa.Boolean(), nullable=False, server_default=sa.false()))
+	user = sa.Table('user', meta,
+		sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+		sa.Column('unix_uid', sa.Integer(), nullable=False),
+		sa.Column('loginname', sa.String(length=32), nullable=False),
+		sa.Column('displayname', sa.String(length=128), nullable=False),
+		sa.Column('primary_email_id', sa.Integer(), nullable=False),
+		sa.Column('recovery_email_id', sa.Integer(), nullable=True),
+		sa.Column('pwhash', sa.Text(), nullable=True),
+		sa.Column('is_service_user', sa.Boolean(), nullable=False),
+		sa.Column('is_deactivated', sa.Boolean(), nullable=False, server_default=sa.false()),
+		sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'),
+		sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
+		sa.ForeignKeyConstraint(['unix_uid'], ['uid_allocation.id'], name=op.f('fk_user_unix_uid_uid_allocation')),
+		sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
+		sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')),
+		sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid'))
+	)
+	with op.batch_alter_table('user', copy_from=user) as batch_op:
+		batch_op.alter_column('is_deactivated', server_default=None)
+
+def downgrade():
+	meta = sa.MetaData(bind=op.get_bind())
+	user = sa.Table('user', meta,
+		sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+		sa.Column('unix_uid', sa.Integer(), nullable=False),
+		sa.Column('loginname', sa.String(length=32), nullable=False),
+		sa.Column('displayname', sa.String(length=128), nullable=False),
+		sa.Column('primary_email_id', sa.Integer(), nullable=False),
+		sa.Column('recovery_email_id', sa.Integer(), nullable=True),
+		sa.Column('pwhash', sa.Text(), nullable=True),
+		sa.Column('is_service_user', sa.Boolean(), nullable=False),
+		sa.Column('is_deactivated', sa.Boolean(), nullable=False),
+		sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'),
+		sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
+		sa.ForeignKeyConstraint(['unix_uid'], ['uid_allocation.id'], name=op.f('fk_user_unix_uid_uid_allocation')),
+		sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
+		sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')),
+		sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid'))
+	)
+	with op.batch_alter_table('user', schema=None) as batch_op:
+		batch_op.drop_column('is_deactivated')
+	service = sa.Table('service', meta,
+		sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+		sa.Column('name', sa.String(length=255), nullable=False),
+		sa.Column('limit_access', sa.Boolean(), nullable=False),
+		sa.Column('access_group_id', sa.Integer(), nullable=True),
+		sa.Column('remailer_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), nullable=False),
+		sa.Column('enable_email_preferences', sa.Boolean(), nullable=False),
+		sa.Column('hide_deactivated_users', sa.Boolean(), nullable=False),
+		sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'),
+		sa.PrimaryKeyConstraint('id', name=op.f('pk_service')),
+		sa.UniqueConstraint('name', name=op.f('uq_service_name'))
+	)
+	with op.batch_alter_table('service', copy_from=service) as batch_op:
+		batch_op.drop_column('hide_deactivated_users')
diff --git a/uffd/models/invite.py b/uffd/models/invite.py
index 27c1db2bbd15a4a4a432908b14cdd984c97c26bd..ef8acc36f7f377c2a814f76882088ab37c8f47aa 100644
--- a/uffd/models/invite.py
+++ b/uffd/models/invite.py
@@ -43,6 +43,8 @@ class Invite(db.Model):
 	def permitted(self):
 		if self.creator is None:
 			return False # Creator does not exist (anymore)
+		if self.creator.is_deactivated:
+			return False
 		if self.creator.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
 			return True
 		if self.allow_signup and not self.creator.is_in_group(current_app.config['ACL_SIGNUP_GROUP']):
diff --git a/uffd/models/service.py b/uffd/models/service.py
index ff31419bc276b07850ce5c619f4e4f6c24380668..1817954c7a0deab79adc915be6ac30151cea50e9 100644
--- a/uffd/models/service.py
+++ b/uffd/models/service.py
@@ -35,6 +35,7 @@ class Service(db.Model):
 
 	remailer_mode = Column(Enum(RemailerMode), default=RemailerMode.DISABLED, nullable=False)
 	enable_email_preferences = Column(Boolean(), default=False, nullable=False)
+	hide_deactivated_users = Column(Boolean(), default=False, nullable=False)
 
 class ServiceUser(db.Model):
 	'''Service-related configuration and state for a user
diff --git a/uffd/models/user.py b/uffd/models/user.py
index e3fa2f6a9116622f74cb7356d87d9a6685de784c..2a9339079f0c9b864e40fd214ea619323a83ee97 100644
--- a/uffd/models/user.py
+++ b/uffd/models/user.py
@@ -168,6 +168,7 @@ class User(db.Model):
 	_password = Column('pwhash', Text(), nullable=True)
 	password = PasswordHashAttribute('_password', LowEntropyPasswordHash)
 	is_service_user = Column(Boolean(), default=False, nullable=False)
+	is_deactivated = Column(Boolean(), default=False, nullable=False)
 	groups = relationship('Group', secondary='user_groups', back_populates='members')
 	roles = relationship('Role', secondary='role_members', back_populates='members')
 
diff --git a/uffd/templates/role/show.html b/uffd/templates/role/show.html
index 3ee88d6908541abcc1e7ab5f0e0ec794b9d036b0..282bbb7c186968fcdbbdaa0afddf7d45a48db961 100644
--- a/uffd/templates/role/show.html
+++ b/uffd/templates/role/show.html
@@ -9,7 +9,7 @@
 
 <form action="{{ url_for("role.update", roleid=role.id) }}" method="POST">
 <div class="align-self-center">
-	<div class="float-sm-right pb-2">
+	<div class="clearfix pb-2"><div class="float-sm-right">
 		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
 		<a href="{{ url_for("role.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
 		{% if role.id %}
@@ -23,7 +23,7 @@
 			<a href="#" class="btn btn-secondary disabled">{{_("Set as default")}}</a>
 			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% endif %}
-	</div>
+	</div></div>
 	<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
 		<li class="nav-item">
 			<a class="nav-link active" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="true">{{_("Settings")}}</a>
diff --git a/uffd/templates/service/show.html b/uffd/templates/service/show.html
index 08d03ba01e0607d68e7ebcac95a29b3b528581c1..08e0629febbba15399c509cf64c895b8660911b6 100644
--- a/uffd/templates/service/show.html
+++ b/uffd/templates/service/show.html
@@ -31,6 +31,13 @@
 			</select>
 		</div>
 
+		<div class="form-group col">
+			<div class="form-check">
+				<input class="form-check-input" type="checkbox" id="hide-deactivated-users" name="hide_deactivated_users" value="1" aria-label="enabled" {{ 'checked' if service.hide_deactivated_users }}>
+				<label class="form-check-label" for="hide-deactivated-users">{{ _('Hide deactivated users from service') }}</label>
+			</div>
+		</div>
+
 		<div class="form-group col">
 			<div class="form-check">
 				<input class="form-check-input" type="checkbox" id="service-enable-email-preferences" name="enable_email_preferences" value="1" aria-label="enabled" {{ 'checked' if service.enable_email_preferences }}>
diff --git a/uffd/templates/user/list.html b/uffd/templates/user/list.html
index 63748d0f3a1971bb008741aafc36730bf6e4633c..6e7d08a2bfc736013e0dee2dd211b2b077ea18d6 100644
--- a/uffd/templates/user/list.html
+++ b/uffd/templates/user/list.html
@@ -33,6 +33,9 @@
 						{% if user.is_service_user %}
 						<span class="badge badge-secondary">{{_('service')}}</span>
 						{% endif %}
+						{% if user.is_deactivated %}
+						<span class="badge badge-danger">{{ _('deactivated') }}</span>
+						{% endif %}
 					</td>
 					<td>
 						{{ user.displayname }}
diff --git a/uffd/templates/user/show.html b/uffd/templates/user/show.html
index 7a4f265d47fd940ef7067c56be836cf2f8a9ad13..04b2cf359c000a995d3d8687ecd3940aed0e86a2 100644
--- a/uffd/templates/user/show.html
+++ b/uffd/templates/user/show.html
@@ -22,15 +22,27 @@
 <form action="{{ url_for("user.create") }}" method="POST">
 {% endif %}
 <div class="align-self-center">
-	<div class="float-sm-right pb-2">
+	{% if user.id and user.is_deactivated %}
+	<div class="alert alert-warning">
+		{{ _('This account is deactivated. The user cannot login and existing sessions are not usable. The user cannot log into services, but existing sessions on services might still be active.') }}
+	</div>
+	{% endif %}
+	<div class="clearfix pb-2"><div class="float-sm-right">
 		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
 		<a href="{{ url_for("user.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
-		{% if user.id %}
+		{% if user.id and not user.is_deactivated and user != request.user %}
+			<a href="{{ url_for("user.deactivate", id=user.id) }}" class="btn btn-secondary">{{ _("Deactivate") }}</a>
+		{% elif user.id and user.is_deactivated %}
+			<a href="{{ url_for("user.activate", id=user.id) }}" class="btn btn-primary">{{ _("Activate") }}</a>
+		{% else %}
+			<a href="#" class="btn btn-secondary disabled">{{ _("Deactivate") }}</a>
+		{% endif %}
+		{% if user.id and user != request.user %}
 			<a href="{{ url_for("user.delete", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% else %}
 			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% endif %}
-	</div>
+	</div></div>
 	<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
 		<li class="nav-item">
 			<a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">{{_("Profile")}}</a>
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
index 18d96bb2f77e2483e726cbbf418892af96e7fd69..68f53c06570d8c2f651b2e70a4884e8674ca69ba 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 667d221f4a330cd57d28e0d83f184245fa7470c5..4f4d621c0e7c7c28629c69dc0a1e8584c4bb0278 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: 2022-11-08 20:12+0100\n"
+"POT-Creation-Date: 2022-11-13 02:05+0100\n"
 "PO-Revision-Date: 2021-05-25 21:18+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: de\n"
@@ -144,8 +144,8 @@ msgstr "Über uffd"
 
 #: uffd/templates/group/list.html:8 uffd/templates/invite/list.html:6
 #: uffd/templates/mail/list.html:8 uffd/templates/role/list.html:8
-#: uffd/templates/service/index.html:8 uffd/templates/service/show.html:99
-#: uffd/templates/service/show.html:127 uffd/templates/user/list.html:8
+#: uffd/templates/service/index.html:8 uffd/templates/service/show.html:106
+#: uffd/templates/service/show.html:134 uffd/templates/user/list.html:8
 msgid "New"
 msgstr "Neu"
 
@@ -162,8 +162,8 @@ msgstr "GID"
 #: uffd/templates/rolemod/list.html:9 uffd/templates/rolemod/show.html:44
 #: uffd/templates/selfservice/self.html:190
 #: uffd/templates/service/index.html:14 uffd/templates/service/show.html:20
-#: uffd/templates/service/show.html:133 uffd/templates/user/show.html:193
-#: uffd/templates/user/show.html:225
+#: uffd/templates/service/show.html:140 uffd/templates/user/show.html:205
+#: uffd/templates/user/show.html:237
 msgid "Name"
 msgstr "Name"
 
@@ -171,14 +171,14 @@ msgstr "Name"
 #: uffd/templates/invite/new.html:36 uffd/templates/role/list.html:15
 #: uffd/templates/role/show.html:48 uffd/templates/rolemod/list.html:10
 #: uffd/templates/rolemod/show.html:26 uffd/templates/selfservice/self.html:191
-#: uffd/templates/user/show.html:194 uffd/templates/user/show.html:226
+#: uffd/templates/user/show.html:206 uffd/templates/user/show.html:238
 msgid "Description"
 msgstr "Beschreibung"
 
 #: uffd/templates/group/show.html:8 uffd/templates/mail/show.html:27
 #: uffd/templates/role/show.html:13 uffd/templates/rolemod/show.html:8
 #: uffd/templates/service/api.html:15 uffd/templates/service/oauth2.html:15
-#: uffd/templates/service/show.html:16 uffd/templates/user/show.html:26
+#: uffd/templates/service/show.html:16 uffd/templates/user/show.html:31
 msgid "Save"
 msgstr "Speichern"
 
@@ -189,7 +189,7 @@ msgstr "Speichern"
 #: uffd/templates/service/show.html:10
 #: uffd/templates/session/deviceauth.html:39
 #: uffd/templates/session/deviceauth.html:49
-#: uffd/templates/session/devicelogin.html:29 uffd/templates/user/show.html:27
+#: uffd/templates/session/devicelogin.html:29 uffd/templates/user/show.html:32
 msgid "Cancel"
 msgstr "Abbrechen"
 
@@ -197,7 +197,7 @@ msgstr "Abbrechen"
 #: uffd/templates/role/show.html:21 uffd/templates/selfservice/self.html:61
 #: uffd/templates/selfservice/self.html:205 uffd/templates/service/api.html:11
 #: uffd/templates/service/oauth2.html:11 uffd/templates/service/show.html:12
-#: uffd/templates/user/show.html:29 uffd/templates/user/show.html:181
+#: uffd/templates/user/show.html:41 uffd/templates/user/show.html:193
 msgid "Are you sure?"
 msgstr "Wirklich fortfahren?"
 
@@ -207,8 +207,8 @@ msgstr "Wirklich fortfahren?"
 #: uffd/templates/role/show.html:21 uffd/templates/role/show.html:24
 #: uffd/templates/selfservice/self.html:62 uffd/templates/service/api.html:12
 #: uffd/templates/service/oauth2.html:12 uffd/templates/service/show.html:13
-#: uffd/templates/user/show.html:29 uffd/templates/user/show.html:31
-#: uffd/templates/user/show.html:104
+#: uffd/templates/user/show.html:41 uffd/templates/user/show.html:43
+#: uffd/templates/user/show.html:116
 msgid "Delete"
 msgstr "Löschen"
 
@@ -238,7 +238,7 @@ msgid "Created by"
 msgstr "Erstellt durch"
 
 #: uffd/templates/invite/list.html:14 uffd/templates/service/api.html:34
-#: uffd/templates/service/show.html:134
+#: uffd/templates/service/show.html:141
 msgid "Permissions"
 msgstr "Berechtigungen"
 
@@ -266,7 +266,7 @@ msgstr "Account-Registrierung"
 msgid "user signups"
 msgstr "Account-Registrierungen"
 
-#: uffd/templates/invite/list.html:49 uffd/templates/user/show.html:178
+#: uffd/templates/invite/list.html:49 uffd/templates/user/show.html:190
 msgid "Disabled"
 msgstr "Deaktiviert"
 
@@ -460,7 +460,7 @@ msgid "One address per line"
 msgstr "Eine Adresse pro Zeile"
 
 #: uffd/templates/mfa/auth.html:6 uffd/templates/selfservice/self.html:159
-#: uffd/templates/user/show.html:176
+#: uffd/templates/user/show.html:188
 msgid "Two-Factor Authentication"
 msgstr "Zwei-Faktor-Authentifizierung"
 
@@ -599,7 +599,7 @@ msgid "Reset two-factor configuration"
 msgstr "Zwei-Faktor-Authentifizierung zurücksetzen"
 
 #: uffd/templates/mfa/setup.html:46 uffd/templates/mfa/setup_recovery.html:5
-#: uffd/templates/user/show.html:179
+#: uffd/templates/user/show.html:191
 msgid "Recovery Codes"
 msgstr "Wiederherstellungscodes"
 
@@ -637,7 +637,7 @@ msgstr "Generiere neue Wiederherstellungscodes"
 msgid "You have no remaining recovery codes."
 msgstr "Du hast keine Wiederherstellungscodes übrig."
 
-#: uffd/templates/mfa/setup.html:85 uffd/templates/user/show.html:179
+#: uffd/templates/mfa/setup.html:85 uffd/templates/user/show.html:191
 msgid "Authenticator Apps (TOTP)"
 msgstr "Authentifikator-Apps (TOTP)"
 
@@ -667,7 +667,7 @@ msgstr "Registriert am"
 msgid "No authenticator apps registered yet"
 msgstr "Bisher keine Authentifikator-Apps registriert"
 
-#: uffd/templates/mfa/setup.html:134 uffd/templates/user/show.html:179
+#: uffd/templates/mfa/setup.html:134 uffd/templates/user/show.html:191
 msgid "U2F and FIDO2 Devices"
 msgstr "U2F und FIDO2 Geräte"
 
@@ -972,7 +972,7 @@ msgstr "Passwort vergessen"
 #: uffd/templates/selfservice/forgot_password.html:9
 #: uffd/templates/selfservice/self.html:21 uffd/templates/session/login.html:12
 #: uffd/templates/signup/start.html:9 uffd/templates/user/list.html:18
-#: uffd/templates/user/show.html:66
+#: uffd/templates/user/show.html:78
 msgid "Login Name"
 msgstr "Anmeldename"
 
@@ -993,7 +993,7 @@ msgstr ""
 "Authentifizierung.\n"
 "\tDiese Berechtigungen werden erst aktiv, wenn du dies getan hast."
 
-#: uffd/templates/selfservice/self.html:14 uffd/templates/user/show.html:36
+#: uffd/templates/selfservice/self.html:14 uffd/templates/user/show.html:48
 msgid "Profile"
 msgstr "Profil"
 
@@ -1010,7 +1010,7 @@ msgid "Changes may take several minutes to be visible in all services."
 msgstr "Änderungen sind erst nach einigen Minuten in allen Diensten sichtbar."
 
 #: uffd/templates/selfservice/self.html:25 uffd/templates/signup/start.html:22
-#: uffd/templates/user/list.html:19 uffd/templates/user/show.html:81
+#: uffd/templates/user/list.html:19 uffd/templates/user/show.html:93
 msgid "Display Name"
 msgstr "Anzeigename"
 
@@ -1018,7 +1018,7 @@ msgstr "Anzeigename"
 msgid "Update Profile"
 msgstr "Änderungen speichern"
 
-#: uffd/templates/selfservice/self.html:37 uffd/templates/user/show.html:98
+#: uffd/templates/selfservice/self.html:37 uffd/templates/user/show.html:110
 msgid "E-Mail Addresses"
 msgstr "E-Mail-Adressen"
 
@@ -1043,7 +1043,7 @@ msgstr "Neue E-Mail-Adresse"
 msgid "Add address"
 msgstr "Adresse hinzufügen"
 
-#: uffd/templates/selfservice/self.html:55 uffd/templates/user/show.html:114
+#: uffd/templates/selfservice/self.html:55 uffd/templates/user/show.html:126
 msgid "primary"
 msgstr "primär"
 
@@ -1090,8 +1090,8 @@ msgid "Address for Password Reset E-Mails"
 msgstr "Adresse für Passwort-Zurücksetzen-E-Mails"
 
 #: uffd/templates/selfservice/self.html:103
-#: uffd/templates/selfservice/self.html:116 uffd/templates/user/show.html:143
-#: uffd/templates/user/show.html:153
+#: uffd/templates/selfservice/self.html:116 uffd/templates/user/show.html:155
+#: uffd/templates/user/show.html:165
 msgid "Use primary address"
 msgstr "Primäre Adresse verwenden"
 
@@ -1110,7 +1110,7 @@ msgstr "E-Mail-Einstellungen speichern"
 
 #: uffd/templates/selfservice/self.html:136
 #: uffd/templates/session/login.html:16 uffd/templates/signup/start.html:36
-#: uffd/templates/user/show.html:163
+#: uffd/templates/user/show.html:175
 msgid "Password"
 msgstr "Passwort"
 
@@ -1155,7 +1155,7 @@ msgid "Manage two-factor authentication"
 msgstr "Zwei-Faktor-Authentifizierung (2FA) verwalten"
 
 #: uffd/templates/selfservice/self.html:178 uffd/templates/user/list.html:20
-#: uffd/templates/user/show.html:39 uffd/templates/user/show.html:188
+#: uffd/templates/user/show.html:51 uffd/templates/user/show.html:200
 #: uffd/views/role.py:21
 msgid "Roles"
 msgstr "Rollen"
@@ -1231,7 +1231,7 @@ msgstr "Zugriff auf Mail-Weiterleitungen"
 msgid "Resolve remailer addresses"
 msgstr "Auflösen von Remailer-Adressen"
 
-#: uffd/templates/service/api.html:51 uffd/templates/service/show.html:48
+#: uffd/templates/service/api.html:51 uffd/templates/service/show.html:55
 msgid "This option has no effect: Remailer config options are unset"
 msgstr "Diese Option hat keine Auswirkung: Remailer ist nicht konfiguriert"
 
@@ -1239,7 +1239,7 @@ msgstr "Diese Option hat keine Auswirkung: Remailer ist nicht konfiguriert"
 msgid "Access uffd metrics"
 msgstr "Zugriff auf uffd-Metriken"
 
-#: uffd/templates/service/oauth2.html:20 uffd/templates/service/show.html:105
+#: uffd/templates/service/oauth2.html:20 uffd/templates/service/show.html:112
 msgid "Client ID"
 msgstr "Client-ID"
 
@@ -1286,8 +1286,8 @@ msgstr "Kein Zugriff"
 msgid "Manage OAuth2 and API clients"
 msgstr "OAuth2- und API-Clients verwalten"
 
-#: uffd/templates/service/overview.html:95 uffd/templates/user/list.html:58
-#: uffd/templates/user/list.html:79
+#: uffd/templates/service/overview.html:95 uffd/templates/user/list.html:61
+#: uffd/templates/user/list.html:82
 msgid "Close"
 msgstr "Schließen"
 
@@ -1309,6 +1309,10 @@ msgid "Members of group \"%(group_name)s\" have access"
 msgstr "Mitglieder der Gruppe \"%(group_name)s\" haben Zugriff"
 
 #: uffd/templates/service/show.html:37
+msgid "Hide deactivated users from service"
+msgstr "Deaktivierte Nutzer verstecken"
+
+#: uffd/templates/service/show.html:44
 msgid ""
 "Allow users with access to select a different e-mail address for this "
 "service"
@@ -1316,29 +1320,29 @@ msgstr ""
 "Ermögliche Nutzern mit Zugriff auf diesen Dienst eine andere E-Mail-"
 "Adresse auszuwählen"
 
-#: uffd/templates/service/show.html:39
+#: uffd/templates/service/show.html:46
 msgid "If disabled, the service always uses the primary e-mail address."
 msgstr "Wenn deaktiviert, wird immer die primäre E-Mail-Adresse verwendet."
 
-#: uffd/templates/service/show.html:46
+#: uffd/templates/service/show.html:53
 msgid "Hide e-mail addresses with remailer"
 msgstr "E-Mail-Adressen mit Remailer verstecken"
 
-#: uffd/templates/service/show.html:53 uffd/templates/service/show.html:76
+#: uffd/templates/service/show.html:60 uffd/templates/service/show.html:83
 msgid "Remailer disabled"
 msgstr "Remailer deaktiviert"
 
-#: uffd/templates/service/show.html:56 uffd/templates/service/show.html:79
+#: uffd/templates/service/show.html:63 uffd/templates/service/show.html:86
 msgid "Remailer enabled"
 msgstr "Remailer aktiviert"
 
-#: uffd/templates/service/show.html:59 uffd/templates/service/show.html:82
+#: uffd/templates/service/show.html:66 uffd/templates/service/show.html:89
 msgid "Remailer enabled (deprecated, case-sensitive format)"
 msgstr ""
 "Remailer aktiviert (veraltetes, Groß-/Kleinschreibung-unterscheidendes "
 "Format)"
 
-#: uffd/templates/service/show.html:63
+#: uffd/templates/service/show.html:70
 msgid ""
 "Some services notify users about changes to their e-mail address. "
 "Modifying this setting immediatly affects the e-mail addresses of all "
@@ -1349,15 +1353,15 @@ msgstr ""
 "-Mail-Adressen aller Nutzer aus und kann zu massenhaftem Versand von "
 "Benachrichtigungs-E-Mails führen."
 
-#: uffd/templates/service/show.html:69
+#: uffd/templates/service/show.html:76
 msgid "Overwrite remailer setting for specific users"
 msgstr "Überschreibe Remailer-Einstellung für ausgewählte Nutzer"
 
-#: uffd/templates/service/show.html:72
+#: uffd/templates/service/show.html:79
 msgid "Login names"
 msgstr "Anmeldenamen"
 
-#: uffd/templates/service/show.html:87
+#: uffd/templates/service/show.html:94
 msgid ""
 "Useful for testing remailer before enabling it for all users. Specify "
 "users as a comma-seperated list of login names."
@@ -1467,7 +1471,7 @@ msgstr "Überprüfen"
 msgid "At least one and at most 128 characters, no other special requirements."
 msgstr "Mindestens 1 und maximal 128 Zeichen, keine weiteren Einschränkungen."
 
-#: uffd/templates/signup/start.html:29 uffd/templates/user/show.html:90
+#: uffd/templates/signup/start.html:29 uffd/templates/user/show.html:102
 msgid "E-Mail Address"
 msgstr "E-Mail-Adresse"
 
@@ -1527,15 +1531,19 @@ msgstr "CSV-Import"
 msgid "UID"
 msgstr "UID"
 
-#: uffd/templates/user/list.html:34 uffd/templates/user/show.html:48
+#: uffd/templates/user/list.html:34 uffd/templates/user/show.html:60
 msgid "service"
 msgstr "service"
 
-#: uffd/templates/user/list.html:57
+#: uffd/templates/user/list.html:37
+msgid "deactivated"
+msgstr "deaktiviert"
+
+#: uffd/templates/user/list.html:60
 msgid "Import a csv formated list of users"
 msgstr "Importiere eine als CSV formatierte Liste von Accounts"
 
-#: uffd/templates/user/list.html:64
+#: uffd/templates/user/list.html:67
 msgid ""
 "The format should be \"loginname,mailaddres,roleid1;roleid2\". Neither "
 "setting the display name nor setting passwords is supported (yet). "
@@ -1545,11 +1553,11 @@ msgstr ""
 "Anzeigename oder das Password können (derzeit) nicht gesetzt werden. "
 "Beispiel:"
 
-#: uffd/templates/user/list.html:75 uffd/templates/user/show.html:76
+#: uffd/templates/user/list.html:78 uffd/templates/user/show.html:88
 msgid "Ignore login name blocklist"
 msgstr "Liste der nicht erlaubten Anmeldenamen ignorieren"
 
-#: uffd/templates/user/list.html:80
+#: uffd/templates/user/list.html:83
 msgid "Import"
 msgstr "Importieren"
 
@@ -1557,19 +1565,38 @@ msgstr "Importieren"
 msgid "New address"
 msgstr "Neue Adresse"
 
-#: uffd/templates/user/show.html:46
+#: uffd/templates/user/show.html:27
+msgid ""
+"This account is deactivated. The user cannot login and existing sessions "
+"are not usable. The user cannot log into services, but existing sessions "
+"on services might still be active."
+msgstr ""
+"Dieser Account ist deaktiviert. Der Nutzer kann sich nicht neu anmelden. "
+"Existierende Sitzungen sind nicht nutzbar. Eine Anmeldung bei Diensten "
+"ist nicht möglich, allerdings könnten bestehende Sitzungen weiterhin "
+"aktiv sein."
+
+#: uffd/templates/user/show.html:34 uffd/templates/user/show.html:38
+msgid "Deactivate"
+msgstr "Deaktivieren"
+
+#: uffd/templates/user/show.html:36
+msgid "Activate"
+msgstr "Aktivieren"
+
+#: uffd/templates/user/show.html:58
 msgid "User ID"
 msgstr "Account ID"
 
-#: uffd/templates/user/show.html:54
+#: uffd/templates/user/show.html:66
 msgid "will be choosen"
 msgstr "wird automatisch bestimmt"
 
-#: uffd/templates/user/show.html:61
+#: uffd/templates/user/show.html:73
 msgid "Service User"
 msgstr "Service-Account"
 
-#: uffd/templates/user/show.html:69
+#: uffd/templates/user/show.html:81
 msgid ""
 "Only letters, numbers, dashes (\"-\") and underscores (\"_\") are "
 "allowed. At most 32, at least 2 characters. There is a word blocklist. "
@@ -1579,7 +1606,7 @@ msgstr ""
 "erlaubt. Maximal 32, mindestens 2 Zeichen. Es gibt eine Liste nicht "
 "erlaubter Namen. Muss einmalig sein."
 
-#: uffd/templates/user/show.html:84
+#: uffd/templates/user/show.html:96
 msgid ""
 "If you leave this empty it will be set to the login name. At most 128, at"
 " least 2 characters. No character restrictions."
@@ -1587,7 +1614,7 @@ msgstr ""
 "Wenn das Feld leer bleibt, wird der Anmeldename verwendet. Maximal 128, "
 "mindestens 2 Zeichen. Keine Zeichenbeschränkung."
 
-#: uffd/templates/user/show.html:93
+#: uffd/templates/user/show.html:105
 msgid ""
 "Make sure the address is correct! Services might use e-mail addresses as "
 "account identifiers and rely on them being unique and verified."
@@ -1596,15 +1623,15 @@ msgstr ""
 " E-Mail-Adresse um Accounts zu identifizieren und verlassen sich darauf, "
 "dass diese verifiziert und einzigartig sind."
 
-#: uffd/templates/user/show.html:102
+#: uffd/templates/user/show.html:114
 msgid "Address"
 msgstr "Adresse"
 
-#: uffd/templates/user/show.html:103
+#: uffd/templates/user/show.html:115
 msgid "Verified"
 msgstr "Verifiziert"
 
-#: uffd/templates/user/show.html:129
+#: uffd/templates/user/show.html:141
 msgid ""
 "Make sure that addresses you add are correct! Services might use e-mail "
 "addresses as account identifiers and rely on them being unique and "
@@ -1614,36 +1641,36 @@ msgstr ""
 "Dienste verwenden die E-Mail-Adresse um Accounts zu identifizieren und "
 "verlassen sich darauf, dass diese verifiziert und einzigartig sind."
 
-#: uffd/templates/user/show.html:133
+#: uffd/templates/user/show.html:145
 msgid "Primary E-Mail Address"
 msgstr "Primäre E-Mail-Adresse"
 
-#: uffd/templates/user/show.html:141
+#: uffd/templates/user/show.html:153
 msgid "Recovery E-Mail Address"
 msgstr "Wiederherstellungs-E-Mail-Adresse"
 
-#: uffd/templates/user/show.html:151
+#: uffd/templates/user/show.html:163
 #, python-format
 msgid "Address for %(name)s"
 msgstr "Adresse für %(name)s"
 
-#: uffd/templates/user/show.html:167
+#: uffd/templates/user/show.html:179
 msgid "E-Mail to set it will be sent"
 msgstr "Mail zum Setzen wird versendet"
 
-#: uffd/templates/user/show.html:178
+#: uffd/templates/user/show.html:190
 msgid "Status:"
 msgstr "Status:"
 
-#: uffd/templates/user/show.html:178
+#: uffd/templates/user/show.html:190
 msgid "Enabled"
 msgstr "Aktiv"
 
-#: uffd/templates/user/show.html:181
+#: uffd/templates/user/show.html:193
 msgid "Reset 2FA"
 msgstr "2FA zurücksetzen"
 
-#: uffd/templates/user/show.html:221
+#: uffd/templates/user/show.html:233
 msgid "Resulting groups (only updated after save)"
 msgstr "Resultierende Gruppen (wird nur aktualisiert beim Speichern)"
 
@@ -1785,8 +1812,8 @@ msgstr ""
 msgid "Two-factor authentication failed"
 msgstr "Zwei-Faktor-Authentifizierung fehlgeschlagen"
 
-#: uffd/views/oauth2.py:167 uffd/views/selfservice.py:66
-#: uffd/views/session.py:72
+#: uffd/views/oauth2.py:172 uffd/views/selfservice.py:66
+#: uffd/views/session.py:67
 #, python-format
 msgid ""
 "We received too many requests from your ip address/network! Please wait "
@@ -1795,19 +1822,19 @@ msgstr ""
 "Wir haben zu viele Anfragen von deiner IP-Adresses bzw. aus deinem "
 "Netzwerk empfangen! Bitte warte mindestens %(delay)s."
 
-#: uffd/views/oauth2.py:175
+#: uffd/views/oauth2.py:180
 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/views/oauth2.py:188
+#: uffd/views/oauth2.py:193
 msgid "Device login failed"
 msgstr "Gerätelogin fehlgeschlagen"
 
-#: uffd/views/oauth2.py:194
+#: uffd/views/oauth2.py:199
 msgid "You need to login to access this service"
 msgstr "Du musst dich anmelden, um auf diesen Dienst zugreifen zu können"
 
-#: uffd/views/oauth2.py:201
+#: uffd/views/oauth2.py:206
 #, python-format
 msgid ""
 "You don't have the permission to access the service "
@@ -1933,7 +1960,7 @@ msgstr "Rolle %(role_name)s verlassen"
 msgid "Services"
 msgstr "Dienste"
 
-#: uffd/views/session.py:70
+#: uffd/views/session.py:65
 #, python-format
 msgid ""
 "We received too many invalid login attempts for this user! Please wait at"
@@ -1942,27 +1969,34 @@ msgstr ""
 "Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account "
 "erhalten! Bitte warte mindestens %(delay)s."
 
-#: uffd/views/session.py:78
+#: uffd/views/session.py:74
 msgid "Login name or password is wrong"
 msgstr "Der Anmeldename oder das Passwort ist falsch"
 
-#: uffd/views/session.py:84
+#: uffd/views/session.py:77
+#, python-format
+msgid "Your account is deactivated. Contact %(contact_email)s for details."
+msgstr ""
+"Dein Account ist deaktiviert. Kontaktiere %(contact_email)s für weitere "
+"Informationen."
+
+#: uffd/views/session.py:83
 msgid "You do not have access to this service"
 msgstr "Du hast keinen Zugriff auf diesen Service"
 
-#: uffd/views/session.py:96 uffd/views/session.py:107
+#: uffd/views/session.py:95 uffd/views/session.py:106
 msgid "You need to login first"
 msgstr "Du musst dich erst anmelden"
 
-#: uffd/views/session.py:128 uffd/views/session.py:138
+#: uffd/views/session.py:127 uffd/views/session.py:137
 msgid "Initiation code is no longer valid"
 msgstr "Startcode ist nicht mehr gültig"
 
-#: uffd/views/session.py:142
+#: uffd/views/session.py:141
 msgid "Invalid confirmation code"
 msgstr "Ungültiger Bestätigungscode"
 
-#: uffd/views/session.py:154 uffd/views/session.py:165
+#: uffd/views/session.py:153 uffd/views/session.py:164
 msgid "Invalid initiation code"
 msgstr "Ungültiger Startcode"
 
@@ -2019,7 +2053,15 @@ msgstr "Passwort ist ungültig"
 msgid "User updated"
 msgstr "Account aktualisiert"
 
-#: uffd/views/user.py:156
+#: uffd/views/user.py:155
+msgid "User deactivated"
+msgstr "Account deaktiviert"
+
+#: uffd/views/user.py:164
+msgid "User activated"
+msgstr "Account aktiviert"
+
+#: uffd/views/user.py:174
 msgid "Deleted user"
 msgstr "Account gelöscht"
 
diff --git a/uffd/views/api.py b/uffd/views/api.py
index cdab573db85fce5e919682008f01ebaddce9e662..d0be61d4c731842edd6167cbde475fe3bd25dc2e 100644
--- a/uffd/views/api.py
+++ b/uffd/views/api.py
@@ -38,7 +38,11 @@ def generate_group_dict(group):
 	return {
 		'id': group.unix_gid,
 		'name': group.name,
-		'members': [user.loginname for user in group.members]
+		'members': [
+			user.loginname
+			for user in group.members
+			if not user.is_deactivated or not request.api_client.service.hide_deactivated_users
+		]
 	}
 
 @bp.route('/getgroups', methods=['GET', 'POST'])
@@ -57,6 +61,8 @@ def getgroups():
 		query = query.filter(Group.name == values[0])
 	elif key == 'member' and len(values) == 1:
 		query = query.join(Group.members).filter(User.loginname == values[0])
+		if request.api_client.service.hide_deactivated_users:
+			query = query.filter(db.not_(User.is_deactivated))
 	else:
 		abort(400)
 	# Single-result queries perform better without eager loading
@@ -81,6 +87,8 @@ def getusers():
 	key = (list(request.values.keys()) or [None])[0]
 	values = request.values.getlist(key)
 	query = ServiceUser.query.filter_by(service=request.api_client.service).join(ServiceUser.user)
+	if request.api_client.service.hide_deactivated_users:
+		query = query.filter(db.not_(User.is_deactivated))
 	if key is None:
 		pass
 	elif key == 'id' and len(values) == 1:
@@ -117,6 +125,8 @@ def checkpassword():
 	if service_user is None or not service_user.user.password.verify(password):
 		login_ratelimit.log(username)
 		return jsonify(None)
+	if service_user.user.is_deactivated:
+		return jsonify(None)
 	if service_user.user.password.needs_rehash:
 		service_user.user.password = password
 		db.session.commit()
diff --git a/uffd/views/oauth2.py b/uffd/views/oauth2.py
index 1d0df2d02aa82f2b1b812eccb2ac859ad11b1e6e..43d65fa61e5b6b1dd8d253b3a6e53c1bda065c11 100644
--- a/uffd/views/oauth2.py
+++ b/uffd/views/oauth2.py
@@ -85,6 +85,8 @@ class UffdRequestValidator(oauthlib.oauth2.RequestValidator):
 			return False
 		if oauthreq.grant.expired:
 			return False
+		if oauthreq.grant.user.is_deactivated:
+			return False
 		oauthreq.user = oauthreq.grant.user
 		oauthreq.scopes = oauthreq.grant.scopes
 		return True
@@ -130,6 +132,9 @@ class UffdRequestValidator(oauthlib.oauth2.RequestValidator):
 		if tok.expired:
 			oauthreq.error_message = 'Token expired'
 			return False
+		if tok.user.is_deactivated:
+			oauthreq.error_message = 'User deactivated'
+			return False
 		if not set(scopes).issubset(tok.scopes):
 			oauthreq.error_message = 'Scopes invalid'
 			return False
@@ -184,7 +189,7 @@ def authorize():
 		del session['devicelogin_id']
 		del session['devicelogin_secret']
 		del session['devicelogin_confirmation']
-		if not initiation or initiation.expired or not confirmation:
+		if not initiation or initiation.expired or not confirmation or confirmation.user.is_deactivated:
 			flash(_('Device login failed'))
 			return redirect(url_for('session.login', ref=request.full_path, devicelogin=True))
 		credentials['user'] = confirmation.user
diff --git a/uffd/views/selfservice.py b/uffd/views/selfservice.py
index 3459d01adafaa0f1ce5fd43cc91d022e0011ced3..bbf062bfaf3bce1529d1433102a6041d573cb6b3 100644
--- a/uffd/views/selfservice.py
+++ b/uffd/views/selfservice.py
@@ -68,7 +68,7 @@ def forgot_password():
 	reset_ratelimit.log(loginname+'/'+mail)
 	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()
+	user = User.query.filter_by(loginname=loginname, is_deactivated=False).one_or_none()
 	if not user:
 		return redirect(url_for('session.login'))
 	matches = any(map(lambda email: secrets.compare_digest(email.address, mail), user.verified_emails))
diff --git a/uffd/views/service.py b/uffd/views/service.py
index 37fa1f67bedf10adab5c505cec021b117ed85737..374180cae6732000954ae61e6c8c1751b4747c07 100644
--- a/uffd/views/service.py
+++ b/uffd/views/service.py
@@ -80,6 +80,7 @@ def edit_submit(id=None):
 	else:
 		service.limit_access = True
 		service.access_group = Group.query.get(request.form['access-group'])
+	service.hide_deactivated_users = request.form.get('hide_deactivated_users') == '1'
 	service.enable_email_preferences = request.form.get('enable_email_preferences') == '1'
 	service.remailer_mode = RemailerMode[request.form['remailer-mode']]
 	remailer_overwrite_mode = RemailerMode[request.form['remailer-overwrite-mode']]
diff --git a/uffd/views/session.py b/uffd/views/session.py
index 9e45c210220a110de38d83a91e06e9a95de7d814..df3301af7b2982069635f5e3646eb550d44c1c86 100644
--- a/uffd/views/session.py
+++ b/uffd/views/session.py
@@ -25,18 +25,12 @@ def set_request_user():
 	if datetime.datetime.utcnow().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']:
 		return
 	user = User.query.get(session['user_id'])
-	if not user or not user.is_in_group(current_app.config['ACL_ACCESS_GROUP']):
+	if not user or user.is_deactivated or 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
 
-def login_get_user(loginname, password):
-	user = User.query.filter_by(loginname=loginname).one_or_none()
-	if user is None or not user.password.verify(password):
-		return None
-	return user
-
 @bp.route("/logout")
 def logout():
 	# The oauth2 module takes data from `session` and injects it into the url,
@@ -56,6 +50,7 @@ def set_session(user, skip_mfa=False):
 
 @bp.route("/login", methods=('GET', 'POST'))
 def login():
+	# pylint: disable=too-many-return-statements
 	if request.user_pre_mfa:
 		return redirect(url_for('mfa.auth', ref=request.values.get('ref', url_for('index'))))
 	if request.method == 'GET':
@@ -71,12 +66,16 @@ def login():
 		else:
 			flash(_('We received too many requests from your ip address/network! Please wait at least %(delay)s.', delay=format_delay(host_delay)))
 		return render_template('session/login.html', ref=request.values.get('ref'))
-	user = login_get_user(username, password)
-	if user is None:
+
+	user = User.query.filter_by(loginname=username).one_or_none()
+	if user is None or not user.password.verify(password):
 		login_ratelimit.log(username)
 		host_ratelimit.log()
 		flash(_('Login name or password is wrong'))
 		return render_template('session/login.html', ref=request.values.get('ref'))
+	if user.is_deactivated:
+		flash(_('Your account is deactivated. Contact %(contact_email)s for details.', contact_email=current_app.config['ORGANISATION_CONTACT']))
+		return render_template('session/login.html', ref=request.values.get('ref'))
 	if user.password.needs_rehash:
 		user.password = password
 		db.session.commit()
diff --git a/uffd/views/user.py b/uffd/views/user.py
index 04fb953bb333aa3c3fdc0a8b3979aa87c305795b..51d4d64e7f734f2e8f0c4acebcbc800dea50c2f4 100644
--- a/uffd/views/user.py
+++ b/uffd/views/user.py
@@ -146,6 +146,24 @@ def update(id):
 	flash(_('User updated'))
 	return redirect(url_for('user.show', id=user.id))
 
+@bp.route('/<int:id>/deactivate')
+@csrf_protect(blueprint=bp)
+def deactivate(id):
+	user = User.query.get_or_404(id)
+	user.is_deactivated = True
+	db.session.commit()
+	flash(_('User deactivated'))
+	return redirect(url_for('user.show', id=user.id))
+
+@bp.route('/<int:id>/activate')
+@csrf_protect(blueprint=bp)
+def activate(id):
+	user = User.query.get_or_404(id)
+	user.is_deactivated = False
+	db.session.commit()
+	flash(_('User activated'))
+	return redirect(url_for('user.show', id=user.id))
+
 @bp.route("/<int:id>/del")
 @csrf_protect(blueprint=bp)
 def delete(id):