diff --git a/uffd/oauth2/models.py b/uffd/oauth2/models.py
index dea62cd9c1aabd42390367d0c926238c3557cb46..cdc3b9f40e318b2da1030ae7ceb327dc28fd9d88 100644
--- a/uffd/oauth2/models.py
+++ b/uffd/oauth2/models.py
@@ -5,7 +5,7 @@ from uffd.database import db
 from uffd.user.models import User
 
 class OAuth2Client:
-	def __init__(self, client_id, client_secret, redirect_uris, required_group=None):
+	def __init__(self, client_id, client_secret, redirect_uris, required_group=None, logout_urls=None):
 		self.client_id = client_id
 		self.client_secret = client_secret
 		# We only support the Authorization Code Flow for confidential (server-side) clients
@@ -13,6 +13,12 @@ class OAuth2Client:
 		self.redirect_uris = redirect_uris
 		self.default_scopes = ['profile']
 		self.required_group = required_group
+		self.logout_urls = []
+		for url in (logout_urls or []):
+			if isinstance(url, str):
+				self.logout_urls.append(['GET', url])
+			else:
+				self.logout_urls.append(url)
 
 	@classmethod
 	def from_id(cls, client_id):
diff --git a/uffd/oauth2/templates/logout.html b/uffd/oauth2/templates/logout.html
new file mode 100644
index 0000000000000000000000000000000000000000..c414e6a7838289d82d66df673978f209a3b3a803
--- /dev/null
+++ b/uffd/oauth2/templates/logout.html
@@ -0,0 +1,98 @@
+{% 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="CCC logo" src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
+		</div>
+		<div class="col-12">
+			<h2 class="text-center">Logout</h2>
+		</div>
+
+		<div class="col-12">
+			<noscript>
+				<div class="alert alert-warning" role="alert">Javascript is required for automatic logout</div>
+			</noscript>
+			<p>While you successfully logged out of the Single-Sign-On service, you may still be logged in on these services:</p>
+			<ul>
+			{% for client in clients if client.logout_urls %}
+				<li class="client" data-urls='{{ client.logout_urls|tojson }}'>
+					{{ client.client_id }}
+					<span class="status-active spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
+					<i class="status-success fas fa-check d-none"></i>
+					<i class="status-failed fas fa-exclamation d-none"></i>
+				</li>
+			{% endfor %}
+			</ul>
+
+			<p>
+				Please wait until you have been automatically logged out of all services or make sure of this yourself.
+			</p>
+
+			<button id="retry-button" class="btn btn-block btn-primary d-none" disabled>
+				<span id="cont-text">Logging you out on all services ...</span>
+			</button>
+
+			<a href="{{ request.values.get('ref') or '/' }}" class="btn btn-block btn-secondary">
+				<span>Skip this and continue</span>
+			</a>
+
+		</div>
+	</div>
+</div>
+
+<script>
+function logout_services() {
+	$("#retry-button").prop('disabled', true);
+	let all_promises = [];
+	$("li.client").each(function () {
+		let elem = $(this);
+		let p = new Promise(function (resolve, reject) {
+			elem.find('.status-active').removeClass('d-none');
+			elem.find('.status-success').addClass('d-none');
+			elem.find('.status-failed').addClass('d-none');
+			resolve();
+		});
+		elem.data('urls').forEach(function (url) {
+			p = p.then(function () {
+				return fetch(url[1], {method: url[0], credentials: 'include'})
+				.then(function (resp) {
+					if (!resp.ok)
+						throw new Error('Server error');
+				});
+			});
+		});
+		p = p.then(function () {
+			console.log('done', elem);
+			elem.find('.status-active').addClass('d-none');
+			elem.find('.status-success').removeClass('d-none');
+			elem.removeClass('client');
+		})
+		.catch(function (err) {
+			elem.find('.status-active').addClass('d-none');
+			elem.find('.status-failed').removeClass('d-none');
+			console.log(err);
+			throw err;
+		});
+		all_promises.push(p);
+	});
+	Promise.allSettled(all_promises).then(function (results) {
+		console.log(results);
+		for (result of results) {
+			if (result.status == 'rejected')
+				throw result.reason;
+		}
+		$('#cont-text').text('Done, redirecting ...');
+		window.location = {{ (request.values.get('ref') or '/')|tojson }};
+	}).catch(function (err) {
+		$("#retry-button").prop('disabled', false);
+		$('#cont-text').text('Log out failed on some services. Retry?');
+	});
+}
+
+$("#retry-button").removeClass('d-none');
+$("#retry-button").on('click', logout_services);
+logout_services();
+</script>
+{% endblock %}
diff --git a/uffd/oauth2/views.py b/uffd/oauth2/views.py
index 586b11d9f87f3b18b54f713a3070cc279dd07df9..50b808fc3b87d6efda4d817a867915753bd08f52 100644
--- a/uffd/oauth2/views.py
+++ b/uffd/oauth2/views.py
@@ -1,7 +1,7 @@
 import datetime
 import functools
 
-from flask import Blueprint, request, jsonify, render_template
+from flask import Blueprint, request, jsonify, render_template, session, redirect
 from werkzeug.datastructures import ImmutableMultiDict
 
 from flask_oauthlib.provider import OAuth2Provider
@@ -86,6 +86,9 @@ def authorize(*args, **kwargs): # pylint: disable=unused-argument
 	# service access to his data. Since we only have trusted services (the
 	# clients defined in the server config), we don't ask for consent.
 	client = kwargs['request'].client
+	session['oauth2-clients'] = session.get('oauth2-clients', [])
+	if client.client_id not in session['oauth2-clients']:
+		session['oauth2-clients'].append(client.client_id)
 	return client.access_allowed(get_current_user())
 
 @bp.route('/token', methods=['GET', 'POST'])
@@ -113,3 +116,17 @@ def error():
 	err = args.pop('error', 'unknown')
 	error_description = args.pop('error_description', '')
 	return render_template('error.html', error=err, error_description=error_description, args=args)
+
+@bp.app_url_defaults
+def inject_logout_params(endpoint, values):
+	if endpoint != 'oauth2.logout' or not session.get('oauth2-clients'):
+		return
+	values['client_ids'] = ','.join(session['oauth2-clients'])
+
+@bp.route('/logout')
+def logout():
+	if not request.values.get('client_ids'):
+		return redirect(request.values.get('ref', '/'))
+	client_ids = request.values['client_ids'].split(',')
+	clients = [OAuth2Client.from_id(client_id) for client_id in client_ids]
+	return render_template('logout.html', clients=clients)
diff --git a/uffd/session/views.py b/uffd/session/views.py
index 9badeb3ca1d06b0b5060c79a19e32f8fd2550b16..2178055cac75294855382d04c0ced951cd1f8aa9 100644
--- a/uffd/session/views.py
+++ b/uffd/session/views.py
@@ -11,8 +11,11 @@ bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/')
 
 @bp.route("/logout")
 def logout():
+	# The oauth2 module takes data from `session` and injects it into the url,
+	# so we need to build the url BEFORE we clear the session!
+	resp = redirect(url_for('oauth2.logout', ref=url_for('.login')))
 	session.clear()
-	return redirect(url_for('.login'))
+	return resp
 
 @bp.route("/login", methods=('GET', 'POST'))
 def login():