Skip to content
Snippets Groups Projects
Commit fbdc132f authored by Julian's avatar Julian
Browse files

Basic Single-Log-Out

parent a827f27b
No related branches found
No related tags found
No related merge requests found
...@@ -5,7 +5,7 @@ from uffd.database import db ...@@ -5,7 +5,7 @@ from uffd.database import db
from uffd.user.models import User from uffd.user.models import User
class OAuth2Client: 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_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
# We only support the Authorization Code Flow for confidential (server-side) clients # We only support the Authorization Code Flow for confidential (server-side) clients
...@@ -13,6 +13,12 @@ class OAuth2Client: ...@@ -13,6 +13,12 @@ class OAuth2Client:
self.redirect_uris = redirect_uris self.redirect_uris = redirect_uris
self.default_scopes = ['profile'] self.default_scopes = ['profile']
self.required_group = required_group 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 @classmethod
def from_id(cls, client_id): def from_id(cls, client_id):
......
{% 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 %}
import datetime import datetime
import functools 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 werkzeug.datastructures import ImmutableMultiDict
from flask_oauthlib.provider import OAuth2Provider from flask_oauthlib.provider import OAuth2Provider
...@@ -86,6 +86,9 @@ def authorize(*args, **kwargs): # pylint: disable=unused-argument ...@@ -86,6 +86,9 @@ def authorize(*args, **kwargs): # pylint: disable=unused-argument
# service access to his data. Since we only have trusted services (the # service access to his data. Since we only have trusted services (the
# clients defined in the server config), we don't ask for consent. # clients defined in the server config), we don't ask for consent.
client = kwargs['request'].client 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()) return client.access_allowed(get_current_user())
@bp.route('/token', methods=['GET', 'POST']) @bp.route('/token', methods=['GET', 'POST'])
...@@ -113,3 +116,17 @@ def error(): ...@@ -113,3 +116,17 @@ def error():
err = args.pop('error', 'unknown') err = args.pop('error', 'unknown')
error_description = args.pop('error_description', '') error_description = args.pop('error_description', '')
return render_template('error.html', error=err, error_description=error_description, args=args) 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)
...@@ -11,8 +11,11 @@ bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/') ...@@ -11,8 +11,11 @@ bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/')
@bp.route("/logout") @bp.route("/logout")
def 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() session.clear()
return redirect(url_for('.login')) return resp
@bp.route("/login", methods=('GET', 'POST')) @bp.route("/login", methods=('GET', 'POST'))
def login(): def login():
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment