forked from Github-Mirrors/canaille
Implemented RP-initiated logout
This commit is contained in:
parent
b378d27b23
commit
95ec09fe54
15 changed files with 743 additions and 76 deletions
|
@ -11,6 +11,7 @@ Added
|
|||
|
||||
- ``DISABLE_PASSWORD_RESET`` configuration option to disable password recovery. :pr:`46`
|
||||
- ``edit_self`` ACL permission to control user self edition. :pr:`47`
|
||||
- Implemented RP-initiated logout :pr:`54`
|
||||
|
||||
Changed
|
||||
*******
|
||||
|
|
|
@ -327,6 +327,14 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.38 NAME 'oauthAuthorizationCodeID'
|
|||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.39 NAME 'oauthPostLogoutRedirectURI'
|
||||
DESC 'OAuth 2.0 Post logout redirection URI'
|
||||
EQUALITY caseIgnoreMatch
|
||||
ORDERING caseIgnoreOrderingMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
||||
DESC 'OAuth 2.0 Authorization Code'
|
||||
SUP top
|
||||
|
@ -352,7 +360,8 @@ olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
|||
oauthSoftwareID $
|
||||
oauthSoftwareVersion $
|
||||
oauthAudience $
|
||||
oauthPreconsent )
|
||||
oauthPreconsent $
|
||||
oauthPostLogoutRedirectURI )
|
||||
)
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.2 NAME 'oauthAuthorizationCode'
|
||||
|
|
|
@ -324,6 +324,15 @@ attributetypes ( 1.3.6.1.4.1.56207.1.1.38 NAME 'oauthAuthorizationCodeID'
|
|||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
attributetypes ( 1.3.6.1.4.1.56207.1.1.39 NAME 'oauthPostLogoutRedirectURI'
|
||||
DESC 'OAuth 2.0 Post logout redirection URI'
|
||||
EQUALITY caseExactMatch
|
||||
ORDERING caseExactOrderingMatch
|
||||
SUBSTR caseExactSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
objectclass ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
||||
DESC 'OAuth 2.0 Authorization Code'
|
||||
SUP top
|
||||
|
@ -349,7 +358,8 @@ objectclass ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
|||
oauthSoftwareID $
|
||||
oauthSoftwareVersion $
|
||||
oauthAudience $
|
||||
oauthPreconsent )
|
||||
oauthPreconsent $
|
||||
oauthPostLogoutRedirectURI )
|
||||
)
|
||||
X-ORIGIN 'OAuth 2.0' )
|
||||
objectclass ( 1.3.6.1.4.1.56207.1.2.2 NAME 'oauthAuthorizationCode'
|
||||
|
|
|
@ -52,6 +52,11 @@ class ClientAdd(FlaskForm):
|
|||
validators=[wtforms.validators.DataRequired()],
|
||||
render_kw={"placeholder": "https://mydomain.tld/callback"},
|
||||
)
|
||||
post_logout_redirect_uris = wtforms.URLField(
|
||||
_("Post logout redirect URIs"),
|
||||
validators=[wtforms.validators.Optional()],
|
||||
render_kw={"placeholder": "https://mydomain.tld/you-have-been-disconnected"},
|
||||
)
|
||||
grant_type = wtforms.SelectMultipleField(
|
||||
_("Grant types"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
|
@ -163,6 +168,7 @@ def add(user):
|
|||
uri=form["uri"].data,
|
||||
grant_type=form["grant_type"].data,
|
||||
redirect_uris=[form["redirect_uris"].data],
|
||||
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data],
|
||||
response_type=form["response_type"].data,
|
||||
scope=form["scope"].data.split(" "),
|
||||
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
||||
|
@ -209,6 +215,11 @@ def client_edit(client_id):
|
|||
data = dict(client)
|
||||
data["scope"] = " ".join(data["scope"])
|
||||
data["redirect_uris"] = data["redirect_uris"][0]
|
||||
data["post_logout_redirect_uris"] = (
|
||||
data["post_logout_redirect_uris"][0]
|
||||
if data["post_logout_redirect_uris"]
|
||||
else ""
|
||||
)
|
||||
data["preconsent"] = client.preconsent
|
||||
form = ClientAdd(request.form or None, data=data, client=client)
|
||||
|
||||
|
@ -230,6 +241,7 @@ def client_edit(client_id):
|
|||
uri=form["uri"].data,
|
||||
grant_type=form["grant_type"].data,
|
||||
redirect_uris=[form["redirect_uris"].data],
|
||||
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data],
|
||||
response_type=form["response_type"].data,
|
||||
scope=form["scope"].data.split(" "),
|
||||
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
||||
|
|
6
canaille/oidc/forms.py
Normal file
6
canaille/oidc/forms.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import wtforms
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
|
||||
class LogoutForm(FlaskForm):
|
||||
answer = wtforms.SubmitField()
|
|
@ -19,6 +19,7 @@ class Client(LDAPObject, ClientMixin):
|
|||
"contact": "oauthClientContact",
|
||||
"uri": "oauthClientURI",
|
||||
"redirect_uris": "oauthRedirectURIs",
|
||||
"post_logout_redirect_uris": "oauthPostLogoutRedirectURI",
|
||||
"logo_uri": "oauthLogoURI",
|
||||
"issue_date": "oauthIssueDate",
|
||||
"secret": "oauthClientSecret",
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import datetime
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlunsplit
|
||||
|
||||
from authlib.integrations.flask_oauth2 import current_token
|
||||
from authlib.jose import jwk
|
||||
from authlib.jose import jwt
|
||||
from authlib.oauth2 import OAuth2Error
|
||||
from flask import abort
|
||||
from flask import Blueprint
|
||||
|
@ -11,13 +14,16 @@ from flask import jsonify
|
|||
from flask import redirect
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
from flask_babel import gettext
|
||||
from flask_babel import lazy_gettext as _
|
||||
from flask_themer import render_template
|
||||
from werkzeug.datastructures import CombinedMultiDict
|
||||
|
||||
from ..flaskutils import current_user
|
||||
from ..forms import FullLoginForm
|
||||
from ..models import User
|
||||
from .forms import LogoutForm
|
||||
from .models import Client
|
||||
from .models import Consent
|
||||
from .oauth2utils import authorization
|
||||
|
@ -43,6 +49,11 @@ CLAIMS = {
|
|||
}
|
||||
|
||||
|
||||
def get_public_key():
|
||||
with open(current_app.config["JWT"]["PUBLIC_KEY"]) as fd:
|
||||
return fd.read()
|
||||
|
||||
|
||||
@bp.route("/authorize", methods=["GET", "POST"])
|
||||
def authorize():
|
||||
current_app.logger.debug(
|
||||
|
@ -184,10 +195,9 @@ def revoke_token():
|
|||
|
||||
@bp.route("/jwks.json")
|
||||
def jwks():
|
||||
with open(current_app.config["JWT"]["PUBLIC_KEY"]) as fd:
|
||||
pubkey = fd.read()
|
||||
|
||||
obj = jwk.dumps(pubkey, current_app.config["JWT"].get("KTY", DEFAULT_JWT_KTY))
|
||||
obj = jwk.dumps(
|
||||
get_public_key(), current_app.config["JWT"].get("KTY", DEFAULT_JWT_KTY)
|
||||
)
|
||||
return jsonify(
|
||||
{
|
||||
"keys": [
|
||||
|
@ -209,3 +219,121 @@ def userinfo():
|
|||
response = generate_user_info(current_token.subject, current_token.scope[0])
|
||||
current_app.logger.debug("userinfo endpoint response: %s", response)
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
def set_parameter_in_url_query(url, **kwargs):
|
||||
split = list(urlsplit(url))
|
||||
|
||||
parameters = "&".join(f"{key}={value}" for key, value in kwargs.items())
|
||||
|
||||
if split[3]:
|
||||
split[3] = f"{split[3]}&{parameters}"
|
||||
else:
|
||||
split[3] = parameters
|
||||
|
||||
return urlunsplit(split)
|
||||
|
||||
|
||||
@bp.route("/end_session", methods=["GET", "POST"])
|
||||
def end_session():
|
||||
data = CombinedMultiDict((request.args, request.form))
|
||||
user = current_user()
|
||||
|
||||
form = LogoutForm(request.form)
|
||||
form.action = url_for("oidc.oauth.end_session_submit")
|
||||
|
||||
client = None
|
||||
valid_uris = []
|
||||
|
||||
if "client_id" in data:
|
||||
client = Client.get(data["client_id"])
|
||||
if client:
|
||||
valid_uris = client.post_logout_redirect_uris
|
||||
|
||||
if (
|
||||
not data.get("id_token_hint")
|
||||
or (data.get("logout_hint") and data["logout_hint"] != user.uid[0])
|
||||
) and not session.get("end_session_confirmation"):
|
||||
session["end_session_data"] = data
|
||||
return render_template(
|
||||
"oidc/user/logout.html", form=form, client=client, menu=False
|
||||
)
|
||||
|
||||
if data.get("id_token_hint"):
|
||||
id_token = jwt.decode(data["id_token_hint"], get_public_key())
|
||||
if not id_token["iss"] == current_app.config["JWT"]["ISS"]:
|
||||
return jsonify(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "id_token_hint has not been issued here",
|
||||
}
|
||||
)
|
||||
|
||||
if "client_id" in data:
|
||||
if (
|
||||
data["client_id"] != id_token["aud"]
|
||||
and data["client_id"] not in id_token["aud"]
|
||||
):
|
||||
return jsonify(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "id_token_hint and client_id don't match",
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
client_ids = (
|
||||
id_token["aud"]
|
||||
if isinstance(id_token["aud"], list)
|
||||
else [id_token["aud"]]
|
||||
)
|
||||
for client_id in client_ids:
|
||||
client = Client.get(client_id)
|
||||
if client:
|
||||
valid_uris.extend(client.post_logout_redirect_uris)
|
||||
|
||||
if user.uid[0] != id_token["sub"] and not session.get(
|
||||
"end_session_confirmation"
|
||||
):
|
||||
session["end_session_data"] = data
|
||||
return render_template(
|
||||
"oidc/user/logout.html", form=form, client=client, menu=False
|
||||
)
|
||||
|
||||
user.logout()
|
||||
|
||||
if "end_session_confirmation" in session:
|
||||
del session["end_session_confirmation"]
|
||||
|
||||
if (
|
||||
"post_logout_redirect_uri" in data
|
||||
and data["post_logout_redirect_uri"] in valid_uris
|
||||
):
|
||||
url = data["post_logout_redirect_uri"]
|
||||
if "state" in data:
|
||||
url = set_parameter_in_url_query(url, state=data["state"])
|
||||
return redirect(data["post_logout_redirect_uri"])
|
||||
|
||||
flash(_("You have been disconnected"), "success")
|
||||
return redirect(url_for("account.index"))
|
||||
|
||||
|
||||
@bp.route("/end_session_confirm", methods=["POST"])
|
||||
def end_session_submit():
|
||||
form = LogoutForm(request.form)
|
||||
if not form.validate():
|
||||
flash(_("An error happened during the logout"), "error")
|
||||
client = Client.get(session.get("end_session_data", {}).get("client_id"))
|
||||
return render_template("oidc/user/logout.html", form=form, client=client)
|
||||
|
||||
data = session["end_session_data"]
|
||||
del session["end_session_data"]
|
||||
|
||||
if request.form["answer"] == "logout":
|
||||
session["end_session_confirmation"] = True
|
||||
url = set_parameter_in_url_query(url_for("oidc.oauth.end_session"), **data)
|
||||
return redirect(url)
|
||||
|
||||
flash(_("You have not been disconnected"), "info")
|
||||
|
||||
return redirect(url_for("account.index"))
|
||||
|
|
42
canaille/templates/oidc/user/logout.html
Normal file
42
canaille/templates/oidc/user/logout.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends theme('base.html') %}
|
||||
{% import 'fomanticui.html' as sui %}
|
||||
|
||||
{% block content %}
|
||||
<div class="ui segment">
|
||||
<div class="ui center aligned">
|
||||
<h2 class="ui center aligned icon header">
|
||||
<i class="sign out icon"></i>
|
||||
<div class="content">{% trans %}Log out{% endtrans %}</div>
|
||||
<div class="sub header">{% trans %}Do you want to log out?{% endtrans %}</div>
|
||||
</h2>
|
||||
<div class="ui message">
|
||||
<p>
|
||||
{{ _("You are currently logged in as %(username)s.", username=user.name) }}
|
||||
{% if client %}
|
||||
{{ _("The application %(client_name)s want to disconnect your account.", client_name=client.name) }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="ui center aligned container">
|
||||
<form method="POST"
|
||||
id="{{ form.id or form.__class__.__name__|lower }}"
|
||||
action="{{ form.action }}"
|
||||
role="form"
|
||||
class="ui form"
|
||||
>
|
||||
{{ form.hidden_tag() if form.hidden_tag }}
|
||||
<div class="ui stackable buttons">
|
||||
<button name="answer" type="submit" class="ui button" value="stay" id="stay">
|
||||
{% trans %}Stay logged{% endtrans %}
|
||||
</button>
|
||||
<button name="answer" type="submit" class="ui primary button" value="logout" id="logout">
|
||||
{% trans %}Logout{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -3,25 +3,25 @@
|
|||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
|
||||
# Camille <camille@yaal.coop>, 2021-2022.
|
||||
# Éloi Rivard <eloi@yaal.fr>, 2020-2022.
|
||||
# Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2022.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: contact@yaal.fr\n"
|
||||
"POT-Creation-Date: 2022-04-06 17:43+0200\n"
|
||||
"PO-Revision-Date: 2022-04-06 17:44+0200\n"
|
||||
"Last-Translator: Éloi Rivard <eloi@yaal.fr>\n"
|
||||
"Language: fr_FR\n"
|
||||
"Language-Team: French - France <equipe@yaal.fr>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
"POT-Creation-Date: 2022-06-02 15:50+0200\n"
|
||||
"PO-Revision-Date: 2022-06-02 15:55+0200\n"
|
||||
"Last-Translator: Éloi Rivard <eloi.rivard@aquilenet.fr>\n"
|
||||
"Language: fr\n"
|
||||
"Language-Team: French <traduc@traduc.org>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
"Generated-By: Babel 2.10.1\n"
|
||||
"X-Generator: Gtranslator 40.0\n"
|
||||
|
||||
#: canaille/account.py:93 canaille/account.py:118 canaille/oidc/oauth.py:77
|
||||
#: canaille/account.py:93 canaille/account.py:118 canaille/oidc/oauth.py:88
|
||||
msgid "Login failed, please check your information"
|
||||
msgstr "La connexion a échoué, veuillez vérifier vos informations."
|
||||
|
||||
|
@ -338,11 +338,11 @@ msgstr "Le groupe %(group)s a bien été supprimé"
|
|||
msgid "You have been invited to create an account on {website_name}"
|
||||
msgstr "Vous êtes invités à vous créer un compte sur {website_name}"
|
||||
|
||||
#: canaille/ldap_backend/backend.py:54
|
||||
#: canaille/ldap_backend/backend.py:50
|
||||
msgid "Could not connect to the LDAP server '{uri}'"
|
||||
msgstr "Impossible de se connecter au serveur LDAP '{uri}'"
|
||||
|
||||
#: canaille/ldap_backend/backend.py:70
|
||||
#: canaille/ldap_backend/backend.py:66
|
||||
msgid "LDAP authentication failed with user '{user}'"
|
||||
msgstr ""
|
||||
"L'authentification au serveur LDAP a échoué pour l'utilisateur '{user}'"
|
||||
|
@ -360,74 +360,78 @@ msgid "Redirect URIs"
|
|||
msgstr "URIs de redirection"
|
||||
|
||||
#: canaille/oidc/clients.py:56
|
||||
msgid "Post logout redirect URIs"
|
||||
msgstr "URIs de redirection post-déconnexion"
|
||||
|
||||
#: canaille/oidc/clients.py:61
|
||||
msgid "Grant types"
|
||||
msgstr "Grant types"
|
||||
|
||||
#: canaille/oidc/clients.py:68 canaille/templates/oidc/admin/token_view.html:33
|
||||
#: canaille/oidc/clients.py:73 canaille/templates/oidc/admin/token_view.html:33
|
||||
msgid "Scope"
|
||||
msgstr "Scope"
|
||||
|
||||
#: canaille/oidc/clients.py:74
|
||||
#: canaille/oidc/clients.py:79
|
||||
msgid "Response types"
|
||||
msgstr "Types de réponse"
|
||||
|
||||
#: canaille/oidc/clients.py:80
|
||||
#: canaille/oidc/clients.py:85
|
||||
msgid "Token Endpoint Auth Method"
|
||||
msgstr "Token Endpoint Auth Method"
|
||||
|
||||
#: canaille/oidc/clients.py:90
|
||||
#: canaille/oidc/clients.py:95
|
||||
msgid "Token audiences"
|
||||
msgstr "Token audiences"
|
||||
|
||||
#: canaille/oidc/clients.py:96
|
||||
#: canaille/oidc/clients.py:101
|
||||
msgid "Logo URI"
|
||||
msgstr "URI du logo"
|
||||
|
||||
#: canaille/oidc/clients.py:101
|
||||
#: canaille/oidc/clients.py:106
|
||||
msgid "Terms of service URI"
|
||||
msgstr "URI des conditions d'utilisation"
|
||||
|
||||
#: canaille/oidc/clients.py:106
|
||||
#: canaille/oidc/clients.py:111
|
||||
msgid "Policy URI"
|
||||
msgstr "URI de la politique de confidentialité"
|
||||
|
||||
#: canaille/oidc/clients.py:111
|
||||
#: canaille/oidc/clients.py:116
|
||||
msgid "Software ID"
|
||||
msgstr "ID du logiciel"
|
||||
|
||||
#: canaille/oidc/clients.py:116
|
||||
#: canaille/oidc/clients.py:121
|
||||
msgid "Software Version"
|
||||
msgstr "Version du logiciel"
|
||||
|
||||
#: canaille/oidc/clients.py:121
|
||||
#: canaille/oidc/clients.py:126
|
||||
msgid "JWK"
|
||||
msgstr "JWK"
|
||||
|
||||
#: canaille/oidc/clients.py:126
|
||||
#: canaille/oidc/clients.py:131
|
||||
msgid "JKW URI"
|
||||
msgstr "URI du JWK"
|
||||
|
||||
#: canaille/oidc/clients.py:131
|
||||
#: canaille/oidc/clients.py:136
|
||||
msgid "Pre-consent"
|
||||
msgstr "Pré-autorisé"
|
||||
|
||||
#: canaille/oidc/clients.py:149
|
||||
#: canaille/oidc/clients.py:154
|
||||
msgid "The client has not been added. Please check your information."
|
||||
msgstr "Le client n'a pas été ajouté. Veuillez vérifier vos informations."
|
||||
|
||||
#: canaille/oidc/clients.py:184
|
||||
#: canaille/oidc/clients.py:190
|
||||
msgid "The client has been created."
|
||||
msgstr "Le client a été créé."
|
||||
|
||||
#: canaille/oidc/clients.py:222
|
||||
#: canaille/oidc/clients.py:233
|
||||
msgid "The client has not been edited. Please check your information."
|
||||
msgstr "Le client n'a pas été édité. Veuillez vérifier vos informations."
|
||||
|
||||
#: canaille/oidc/clients.py:248
|
||||
#: canaille/oidc/clients.py:260
|
||||
msgid "The client has been edited."
|
||||
msgstr "Le client a été édité."
|
||||
|
||||
#: canaille/oidc/clients.py:264
|
||||
#: canaille/oidc/clients.py:276
|
||||
msgid "The client has been deleted."
|
||||
msgstr "Le client a été supprimé."
|
||||
|
||||
|
@ -439,30 +443,43 @@ msgstr "Impossible de supprimer cet accès."
|
|||
msgid "The access has been revoked"
|
||||
msgstr "L'accès a été révoqué."
|
||||
|
||||
#: canaille/oidc/oauth.py:37
|
||||
#: canaille/oidc/oauth.py:43
|
||||
msgid "Personnal information about yourself, such as your name or your gender."
|
||||
msgstr "Vos informations personnelles, comme votre nom ou votre genre."
|
||||
|
||||
#: canaille/oidc/oauth.py:39
|
||||
#: canaille/oidc/oauth.py:45
|
||||
msgid "Your email address."
|
||||
msgstr "Votre adresse email."
|
||||
|
||||
#: canaille/oidc/oauth.py:40
|
||||
#: canaille/oidc/oauth.py:46
|
||||
msgid "Your postal address."
|
||||
msgstr "Votre adresse postale."
|
||||
|
||||
#: canaille/oidc/oauth.py:41
|
||||
#: canaille/oidc/oauth.py:47
|
||||
msgid "Your phone number."
|
||||
msgstr "Votre numéro de téléphone."
|
||||
|
||||
#: canaille/oidc/oauth.py:42
|
||||
#: canaille/oidc/oauth.py:48
|
||||
msgid "Groups you are belonging to"
|
||||
msgstr "Les groupes auxquels vous appartenez"
|
||||
|
||||
#: canaille/oidc/oauth.py:125
|
||||
#: canaille/oidc/oauth.py:136
|
||||
msgid "You have been successfully logged out."
|
||||
msgstr "Vous avez été déconnecté·e."
|
||||
|
||||
#: canaille/oidc/oauth.py:317
|
||||
#, python-format
|
||||
msgid "You have been disconnected"
|
||||
msgstr "Vous avez été déconnecté·e."
|
||||
|
||||
#: canaille/oidc/oauth.py:325
|
||||
msgid "An error happened during the logout"
|
||||
msgstr "Une erreur est survenue lors de la déconnexion"
|
||||
|
||||
#: canaille/oidc/oauth.py:337
|
||||
msgid "You have not been disconnected"
|
||||
msgstr "Vous n'avez pas été déconnecté"
|
||||
|
||||
#: canaille/templates/about.html:12 canaille/themes/default/base.html:104
|
||||
msgid "About canaille"
|
||||
msgstr "À propos de canaille"
|
||||
|
@ -1242,6 +1259,33 @@ msgid "You did not authorize applications yet."
|
|||
msgstr ""
|
||||
"Vous n'avez pas encore autorisé d'application à accéder à votre profil."
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:9
|
||||
#: canaille/themes/default/base.html:90
|
||||
msgid "Log out"
|
||||
msgstr "Déconnexion"
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:10
|
||||
msgid "Do you want to log out?"
|
||||
msgstr "Voulez vous vous déconnecter ?"
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:14
|
||||
#, python-format
|
||||
msgid "You are currently logged in as %(username)s."
|
||||
msgstr "Vous êtes identifiés en tant que %(username)s"
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:16
|
||||
#, python-format
|
||||
msgid "The application %(client_name)s want to disconnect your account."
|
||||
msgstr "L'application %(client_name)s demande votre déconnexion."
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:33
|
||||
msgid "Stay logged"
|
||||
msgstr "Rester connecté"
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:36
|
||||
msgid "Logout"
|
||||
msgstr "Déconnexion"
|
||||
|
||||
#: canaille/themes/default/base.html:10
|
||||
msgid "authorization interface"
|
||||
msgstr " - Interface de gestion des autorisations"
|
||||
|
@ -1266,10 +1310,6 @@ msgstr "Codes"
|
|||
msgid "Emails"
|
||||
msgstr "Courriels"
|
||||
|
||||
#: canaille/themes/default/base.html:90
|
||||
msgid "Log out"
|
||||
msgstr "Déconnexion"
|
||||
|
||||
#~ msgid "Logged in as"
|
||||
#~ msgstr "Connecté en tant que"
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-06-02 15:41+0200\n"
|
||||
"POT-Creation-Date: 2022-06-02 15:50+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,7 +17,7 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.10.1\n"
|
||||
|
||||
#: canaille/account.py:93 canaille/account.py:118 canaille/oidc/oauth.py:77
|
||||
#: canaille/account.py:93 canaille/account.py:118 canaille/oidc/oauth.py:88
|
||||
msgid "Login failed, please check your information"
|
||||
msgstr ""
|
||||
|
||||
|
@ -342,74 +342,78 @@ msgid "Redirect URIs"
|
|||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:56
|
||||
msgid "Post logout redirect URIs"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:61
|
||||
msgid "Grant types"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:68 canaille/templates/oidc/admin/token_view.html:33
|
||||
#: canaille/oidc/clients.py:73 canaille/templates/oidc/admin/token_view.html:33
|
||||
msgid "Scope"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:74
|
||||
#: canaille/oidc/clients.py:79
|
||||
msgid "Response types"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:80
|
||||
#: canaille/oidc/clients.py:85
|
||||
msgid "Token Endpoint Auth Method"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:90
|
||||
#: canaille/oidc/clients.py:95
|
||||
msgid "Token audiences"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:96
|
||||
#: canaille/oidc/clients.py:101
|
||||
msgid "Logo URI"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:101
|
||||
#: canaille/oidc/clients.py:106
|
||||
msgid "Terms of service URI"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:106
|
||||
#: canaille/oidc/clients.py:111
|
||||
msgid "Policy URI"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:111
|
||||
#: canaille/oidc/clients.py:116
|
||||
msgid "Software ID"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:116
|
||||
#: canaille/oidc/clients.py:121
|
||||
msgid "Software Version"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:121
|
||||
#: canaille/oidc/clients.py:126
|
||||
msgid "JWK"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:126
|
||||
#: canaille/oidc/clients.py:131
|
||||
msgid "JKW URI"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:131
|
||||
#: canaille/oidc/clients.py:136
|
||||
msgid "Pre-consent"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:149
|
||||
#: canaille/oidc/clients.py:154
|
||||
msgid "The client has not been added. Please check your information."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:184
|
||||
#: canaille/oidc/clients.py:190
|
||||
msgid "The client has been created."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:222
|
||||
#: canaille/oidc/clients.py:233
|
||||
msgid "The client has not been edited. Please check your information."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:248
|
||||
#: canaille/oidc/clients.py:260
|
||||
msgid "The client has been edited."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/clients.py:264
|
||||
#: canaille/oidc/clients.py:276
|
||||
msgid "The client has been deleted."
|
||||
msgstr ""
|
||||
|
||||
|
@ -421,30 +425,42 @@ msgstr ""
|
|||
msgid "The access has been revoked"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:37
|
||||
#: canaille/oidc/oauth.py:43
|
||||
msgid "Personnal information about yourself, such as your name or your gender."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:39
|
||||
#: canaille/oidc/oauth.py:45
|
||||
msgid "Your email address."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:40
|
||||
#: canaille/oidc/oauth.py:46
|
||||
msgid "Your postal address."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:41
|
||||
#: canaille/oidc/oauth.py:47
|
||||
msgid "Your phone number."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:42
|
||||
#: canaille/oidc/oauth.py:48
|
||||
msgid "Groups you are belonging to"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:125
|
||||
#: canaille/oidc/oauth.py:136
|
||||
msgid "You have been successfully logged out."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:317
|
||||
msgid "You have been disconnected"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:325
|
||||
msgid "An error happened during the logout"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/oidc/oauth.py:337
|
||||
msgid "You have not been disconnected"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/about.html:12 canaille/themes/default/base.html:104
|
||||
msgid "About canaille"
|
||||
msgstr ""
|
||||
|
@ -1151,6 +1167,33 @@ msgstr ""
|
|||
msgid "You did not authorize applications yet."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:9
|
||||
#: canaille/themes/default/base.html:90
|
||||
msgid "Log out"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:10
|
||||
msgid "Do you want to log out?"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:14
|
||||
#, python-format
|
||||
msgid "You are currently logged in as %(username)s."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:16
|
||||
#, python-format
|
||||
msgid "The application %(client_name)s want to disconnect your account."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:33
|
||||
msgid "Stay logged"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/templates/oidc/user/logout.html:36
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/themes/default/base.html:10
|
||||
msgid "authorization interface"
|
||||
msgstr ""
|
||||
|
@ -1174,7 +1217,3 @@ msgstr ""
|
|||
#: canaille/themes/default/base.html:83
|
||||
msgid "Emails"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/themes/default/base.html:90
|
||||
msgid "Log out"
|
||||
msgstr ""
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlunsplit
|
||||
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from authlib.oidc.discovery import get_well_known_url
|
||||
from flask import current_app
|
||||
from flask import flash
|
||||
from flask import Flask
|
||||
from flask import redirect
|
||||
|
@ -39,6 +43,7 @@ def create_app():
|
|||
def authorize():
|
||||
token = oauth.canaille.authorize_access_token()
|
||||
session["user"] = token.get("userinfo")
|
||||
session["id_token"] = token["id_token"]
|
||||
flash("You have been successfully logged in.", "success")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
@ -49,7 +54,31 @@ def create_app():
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
flash("You have been successfully logged out.", "success")
|
||||
return redirect(url_for("index"))
|
||||
flash("You have been successfully logged out", "success")
|
||||
|
||||
oauth.canaille.load_server_metadata()
|
||||
end_session_endpoint = oauth.canaille.server_metadata.get(
|
||||
"end_session_endpoint"
|
||||
)
|
||||
end_session_url = set_parameter_in_url_query(
|
||||
end_session_endpoint,
|
||||
client_id=current_app.config["OAUTH_CLIENT_ID"],
|
||||
id_token_hint=session["id_token"],
|
||||
post_logout_redirect_uri=url_for("index", _external=True),
|
||||
)
|
||||
return redirect(end_session_url)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def set_parameter_in_url_query(url, **kwargs):
|
||||
split = list(urlsplit(url))
|
||||
|
||||
parameters = "&".join(f"{key}={value}" for key, value in kwargs.items())
|
||||
|
||||
if split[3]:
|
||||
split[3] = f"{split[3]}&{parameters}"
|
||||
else:
|
||||
split[3] = parameters
|
||||
|
||||
return urlunsplit(split)
|
||||
|
|
|
@ -79,6 +79,7 @@ oauthClientName: Client1
|
|||
oauthClientContact: admin@mydomain.tld
|
||||
oauthClientURI: http://localhost:5001
|
||||
oauthRedirectURIs: http://localhost:5001/authorize
|
||||
oauthPostLogoutRedirectURI: http://localhost:5001/
|
||||
oauthGrantType: authorization_code
|
||||
oauthGrantType: refresh_token
|
||||
oauthScope: openid
|
||||
|
@ -98,6 +99,7 @@ oauthClientName: Client2
|
|||
oauthClientContact: admin@mydomain.tld
|
||||
oauthClientURI: http://localhost:5002
|
||||
oauthRedirectURIs: http://localhost:5002/authorize
|
||||
oauthPostLogoutRedirectURI: http://localhost:5002/
|
||||
oauthGrantType: authorization_code
|
||||
oauthGrantType: refresh_token
|
||||
oauthScope: openid
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from authlib.oidc.core.grants.util import generate_id_token
|
||||
from canaille.oidc.models import AuthorizationCode
|
||||
from canaille.oidc.models import Client
|
||||
from canaille.oidc.models import Consent
|
||||
from canaille.oidc.models import Token
|
||||
from canaille.oidc.oauth2utils import generate_user_info
|
||||
from canaille.oidc.oauth2utils import get_jwt_config
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
|
||||
|
@ -35,6 +38,7 @@ def client(testclient, other_client, slapd_connection):
|
|||
policy_uri="https://mydomain.tld/policy",
|
||||
jwk_uri="https://mydomain.tld/jwk",
|
||||
token_endpoint_auth_method="client_secret_basic",
|
||||
post_logout_redirect_uris=["https://mydomain.tld/disconnected"],
|
||||
)
|
||||
c.audience = [c.dn, other_client.dn]
|
||||
c.save()
|
||||
|
@ -73,6 +77,7 @@ def other_client(testclient, slapd_connection):
|
|||
policy_uri="https://myotherdomain.tld/policy",
|
||||
jwk_uri="https://myotherdomain.tld/jwk",
|
||||
token_endpoint_auth_method="client_secret_basic",
|
||||
post_logout_redirect_uris=["https://myotherdomain.tld/disconnected"],
|
||||
)
|
||||
c.audience = [c.dn]
|
||||
c.save()
|
||||
|
@ -131,6 +136,16 @@ def token(testclient, client, user, slapd_connection):
|
|||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def id_token(testclient, client, user, slapd_connection):
|
||||
return generate_id_token(
|
||||
{},
|
||||
generate_user_info(user.dn, client.scope),
|
||||
aud=client.client_id,
|
||||
**get_jwt_config(None)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def consent(testclient, client, user, slapd_connection):
|
||||
t = Consent(
|
||||
|
|
|
@ -38,6 +38,7 @@ def test_client_add(testclient, logged_admin):
|
|||
"jwk_uri": "https://foo.bar/jwks.json",
|
||||
"audience": [],
|
||||
"preconsent": False,
|
||||
"post_logout_redirect_uris": ["https://foo.bar/disconnected"],
|
||||
}
|
||||
for k, v in data.items():
|
||||
res.form[k].force_value(v)
|
||||
|
@ -78,6 +79,7 @@ def test_client_edit(testclient, client, logged_admin, other_client):
|
|||
"jwk_uri": "https://foo.bar/jwks.json",
|
||||
"audience": [client.dn, other_client.dn],
|
||||
"preconsent": True,
|
||||
"post_logout_redirect_uris": ["https://foo.bar/disconnected"],
|
||||
}
|
||||
for k, v in data.items():
|
||||
res.forms["clientadd"][k].force_value(v)
|
||||
|
|
331
tests/oidc/test_end_session.py
Normal file
331
tests/oidc/test_end_session.py
Normal file
|
@ -0,0 +1,331 @@
|
|||
from authlib.oidc.core.grants.util import generate_id_token
|
||||
from canaille.oidc.oauth2utils import generate_user_info
|
||||
from canaille.oidc.oauth2utils import get_jwt_config
|
||||
|
||||
|
||||
def test_end_session(testclient, slapd_connection, logged_user, client, id_token):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
|
||||
assert res.location.startswith(post_logout_redirect_url)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_end_session_no_client_id(
|
||||
testclient, slapd_connection, logged_user, client, id_token
|
||||
):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
|
||||
assert res.location.startswith(post_logout_redirect_url)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_no_redirect_uri_no_redirect(
|
||||
testclient, slapd_connection, logged_user, client, id_token
|
||||
):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
|
||||
assert res.location == "/"
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_bad_redirect_uri_no_redirect(
|
||||
testclient, slapd_connection, logged_user, client, id_token
|
||||
):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/invalid-uri"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
|
||||
assert res.location == "/"
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_no_client_hint_no_redirect(
|
||||
testclient, slapd_connection, logged_user, client, id_token
|
||||
):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
res = res.form.submit(name="answer", value="logout", status=302)
|
||||
res = res.follow(status=302)
|
||||
|
||||
assert res.location == "/"
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_no_jwt_logout(testclient, slapd_connection, logged_user, client):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
res = res.form.submit(name="answer", value="logout", status=302)
|
||||
res = res.follow(status=302)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
assert res.location.startswith(post_logout_redirect_url)
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_no_jwt_no_logout(testclient, slapd_connection, logged_user, client):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
res = res.form.submit(name="answer", value="stay", status=302)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert sess.get("user_dn")
|
||||
|
||||
assert res.location == "/"
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
|
||||
def test_jwt_not_issued_here(
|
||||
testclient, slapd_connection, logged_user, client, id_token
|
||||
):
|
||||
testclient.app.config["JWT"]["ISS"] = "https://foo.bar"
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
assert res.json == {
|
||||
"message": "id_token_hint has not been issued here",
|
||||
"status": "error",
|
||||
}
|
||||
|
||||
|
||||
def test_bad_client_hint(testclient, slapd_connection, logged_user, client):
|
||||
id_token = generate_id_token(
|
||||
{},
|
||||
generate_user_info(logged_user.dn, client.scope),
|
||||
aud="another_client_id",
|
||||
**get_jwt_config(None),
|
||||
)
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
assert res.json == {
|
||||
"message": "id_token_hint and client_id don't match",
|
||||
"status": "error",
|
||||
}
|
||||
|
||||
|
||||
def test_bad_user_id_token_mismatch(
|
||||
testclient, slapd_connection, logged_user, client, admin
|
||||
):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
id_token = generate_id_token(
|
||||
{},
|
||||
generate_user_info(admin.dn, client.scope),
|
||||
aud=client.client_id,
|
||||
**get_jwt_config(None),
|
||||
)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
res = res.form.submit(name="answer", value="logout", status=302)
|
||||
res = res.follow(status=302)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
assert res.location.startswith(post_logout_redirect_url)
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_bad_user_hint(
|
||||
testclient, slapd_connection, logged_user, client, id_token, admin
|
||||
):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"id_token_hint": id_token,
|
||||
"logout_hint": admin.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
res = res.form.submit(name="answer", value="logout", status=302)
|
||||
res = res.follow(status=302)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
assert res.location.startswith(post_logout_redirect_url)
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
||||
|
||||
|
||||
def test_no_jwt_bad_csrf(testclient, slapd_connection, logged_user, client):
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=200)
|
||||
|
||||
post_logout_redirect_url = "https://mydomain.tld/disconnected"
|
||||
res = testclient.get(
|
||||
"/oauth/end_session",
|
||||
params={
|
||||
"logout_hint": logged_user.uid[0],
|
||||
"client_id": client.client_id,
|
||||
"post_logout_redirect_uri": post_logout_redirect_url,
|
||||
"state": "foobar",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
form = res.form
|
||||
form["csrf_token"] = "foobar"
|
||||
res = form.submit(name="answer", value="logout", status=200)
|
||||
|
||||
assert "An error happened during the logout" in res
|
||||
|
||||
res = res.form.submit(name="answer", value="logout", status=302)
|
||||
res = res.follow(status=302)
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
assert not sess.get("user_dn")
|
||||
|
||||
assert res.location.startswith(post_logout_redirect_url)
|
||||
|
||||
testclient.get(f"/profile/{logged_user.uid[0]}", status=403)
|
Loading…
Reference in a new issue