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`
|
- ``DISABLE_PASSWORD_RESET`` configuration option to disable password recovery. :pr:`46`
|
||||||
- ``edit_self`` ACL permission to control user self edition. :pr:`47`
|
- ``edit_self`` ACL permission to control user self edition. :pr:`47`
|
||||||
|
- Implemented RP-initiated logout :pr:`54`
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
*******
|
*******
|
||||||
|
|
|
@ -327,6 +327,14 @@ olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.38 NAME 'oauthAuthorizationCodeID'
|
||||||
SINGLE-VALUE
|
SINGLE-VALUE
|
||||||
USAGE userApplications
|
USAGE userApplications
|
||||||
X-ORIGIN 'OAuth 2.0' )
|
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'
|
olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
||||||
DESC 'OAuth 2.0 Authorization Code'
|
DESC 'OAuth 2.0 Authorization Code'
|
||||||
SUP top
|
SUP top
|
||||||
|
@ -352,7 +360,8 @@ olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
||||||
oauthSoftwareID $
|
oauthSoftwareID $
|
||||||
oauthSoftwareVersion $
|
oauthSoftwareVersion $
|
||||||
oauthAudience $
|
oauthAudience $
|
||||||
oauthPreconsent )
|
oauthPreconsent $
|
||||||
|
oauthPostLogoutRedirectURI )
|
||||||
)
|
)
|
||||||
X-ORIGIN 'OAuth 2.0' )
|
X-ORIGIN 'OAuth 2.0' )
|
||||||
olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.2 NAME 'oauthAuthorizationCode'
|
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
|
SINGLE-VALUE
|
||||||
USAGE userApplications
|
USAGE userApplications
|
||||||
X-ORIGIN 'OAuth 2.0' )
|
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'
|
objectclass ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
||||||
DESC 'OAuth 2.0 Authorization Code'
|
DESC 'OAuth 2.0 Authorization Code'
|
||||||
SUP top
|
SUP top
|
||||||
|
@ -349,7 +358,8 @@ objectclass ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient'
|
||||||
oauthSoftwareID $
|
oauthSoftwareID $
|
||||||
oauthSoftwareVersion $
|
oauthSoftwareVersion $
|
||||||
oauthAudience $
|
oauthAudience $
|
||||||
oauthPreconsent )
|
oauthPreconsent $
|
||||||
|
oauthPostLogoutRedirectURI )
|
||||||
)
|
)
|
||||||
X-ORIGIN 'OAuth 2.0' )
|
X-ORIGIN 'OAuth 2.0' )
|
||||||
objectclass ( 1.3.6.1.4.1.56207.1.2.2 NAME 'oauthAuthorizationCode'
|
objectclass ( 1.3.6.1.4.1.56207.1.2.2 NAME 'oauthAuthorizationCode'
|
||||||
|
|
|
@ -52,6 +52,11 @@ class ClientAdd(FlaskForm):
|
||||||
validators=[wtforms.validators.DataRequired()],
|
validators=[wtforms.validators.DataRequired()],
|
||||||
render_kw={"placeholder": "https://mydomain.tld/callback"},
|
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_type = wtforms.SelectMultipleField(
|
||||||
_("Grant types"),
|
_("Grant types"),
|
||||||
validators=[wtforms.validators.DataRequired()],
|
validators=[wtforms.validators.DataRequired()],
|
||||||
|
@ -163,6 +168,7 @@ def add(user):
|
||||||
uri=form["uri"].data,
|
uri=form["uri"].data,
|
||||||
grant_type=form["grant_type"].data,
|
grant_type=form["grant_type"].data,
|
||||||
redirect_uris=[form["redirect_uris"].data],
|
redirect_uris=[form["redirect_uris"].data],
|
||||||
|
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data],
|
||||||
response_type=form["response_type"].data,
|
response_type=form["response_type"].data,
|
||||||
scope=form["scope"].data.split(" "),
|
scope=form["scope"].data.split(" "),
|
||||||
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
||||||
|
@ -209,6 +215,11 @@ def client_edit(client_id):
|
||||||
data = dict(client)
|
data = dict(client)
|
||||||
data["scope"] = " ".join(data["scope"])
|
data["scope"] = " ".join(data["scope"])
|
||||||
data["redirect_uris"] = data["redirect_uris"][0]
|
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
|
data["preconsent"] = client.preconsent
|
||||||
form = ClientAdd(request.form or None, data=data, client=client)
|
form = ClientAdd(request.form or None, data=data, client=client)
|
||||||
|
|
||||||
|
@ -230,6 +241,7 @@ def client_edit(client_id):
|
||||||
uri=form["uri"].data,
|
uri=form["uri"].data,
|
||||||
grant_type=form["grant_type"].data,
|
grant_type=form["grant_type"].data,
|
||||||
redirect_uris=[form["redirect_uris"].data],
|
redirect_uris=[form["redirect_uris"].data],
|
||||||
|
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data],
|
||||||
response_type=form["response_type"].data,
|
response_type=form["response_type"].data,
|
||||||
scope=form["scope"].data.split(" "),
|
scope=form["scope"].data.split(" "),
|
||||||
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
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",
|
"contact": "oauthClientContact",
|
||||||
"uri": "oauthClientURI",
|
"uri": "oauthClientURI",
|
||||||
"redirect_uris": "oauthRedirectURIs",
|
"redirect_uris": "oauthRedirectURIs",
|
||||||
|
"post_logout_redirect_uris": "oauthPostLogoutRedirectURI",
|
||||||
"logo_uri": "oauthLogoURI",
|
"logo_uri": "oauthLogoURI",
|
||||||
"issue_date": "oauthIssueDate",
|
"issue_date": "oauthIssueDate",
|
||||||
"secret": "oauthClientSecret",
|
"secret": "oauthClientSecret",
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
from urllib.parse import urlunsplit
|
||||||
|
|
||||||
from authlib.integrations.flask_oauth2 import current_token
|
from authlib.integrations.flask_oauth2 import current_token
|
||||||
from authlib.jose import jwk
|
from authlib.jose import jwk
|
||||||
|
from authlib.jose import jwt
|
||||||
from authlib.oauth2 import OAuth2Error
|
from authlib.oauth2 import OAuth2Error
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
@ -11,13 +14,16 @@ from flask import jsonify
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask import session
|
from flask import session
|
||||||
|
from flask import url_for
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
from werkzeug.datastructures import CombinedMultiDict
|
||||||
|
|
||||||
from ..flaskutils import current_user
|
from ..flaskutils import current_user
|
||||||
from ..forms import FullLoginForm
|
from ..forms import FullLoginForm
|
||||||
from ..models import User
|
from ..models import User
|
||||||
|
from .forms import LogoutForm
|
||||||
from .models import Client
|
from .models import Client
|
||||||
from .models import Consent
|
from .models import Consent
|
||||||
from .oauth2utils import authorization
|
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"])
|
@bp.route("/authorize", methods=["GET", "POST"])
|
||||||
def authorize():
|
def authorize():
|
||||||
current_app.logger.debug(
|
current_app.logger.debug(
|
||||||
|
@ -184,10 +195,9 @@ def revoke_token():
|
||||||
|
|
||||||
@bp.route("/jwks.json")
|
@bp.route("/jwks.json")
|
||||||
def jwks():
|
def jwks():
|
||||||
with open(current_app.config["JWT"]["PUBLIC_KEY"]) as fd:
|
obj = jwk.dumps(
|
||||||
pubkey = fd.read()
|
get_public_key(), current_app.config["JWT"].get("KTY", DEFAULT_JWT_KTY)
|
||||||
|
)
|
||||||
obj = jwk.dumps(pubkey, current_app.config["JWT"].get("KTY", DEFAULT_JWT_KTY))
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"keys": [
|
"keys": [
|
||||||
|
@ -209,3 +219,121 @@ def userinfo():
|
||||||
response = generate_user_info(current_token.subject, current_token.scope[0])
|
response = generate_user_info(current_token.subject, current_token.scope[0])
|
||||||
current_app.logger.debug("userinfo endpoint response: %s", response)
|
current_app.logger.debug("userinfo endpoint response: %s", response)
|
||||||
return jsonify(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.
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
|
||||||
# Camille <camille@yaal.coop>, 2021-2022.
|
# Camille <camille@yaal.coop>, 2021-2022.
|
||||||
# Éloi Rivard <eloi@yaal.fr>, 2020-2022.
|
# Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2022.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: contact@yaal.fr\n"
|
"Report-Msgid-Bugs-To: contact@yaal.fr\n"
|
||||||
"POT-Creation-Date: 2022-04-06 17:43+0200\n"
|
"POT-Creation-Date: 2022-06-02 15:50+0200\n"
|
||||||
"PO-Revision-Date: 2022-04-06 17:44+0200\n"
|
"PO-Revision-Date: 2022-06-02 15:55+0200\n"
|
||||||
"Last-Translator: Éloi Rivard <eloi@yaal.fr>\n"
|
"Last-Translator: Éloi Rivard <eloi.rivard@aquilenet.fr>\n"
|
||||||
"Language: fr_FR\n"
|
"Language: fr\n"
|
||||||
"Language-Team: French - France <equipe@yaal.fr>\n"
|
"Language-Team: French <traduc@traduc.org>\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
||||||
"MIME-Version: 1.0\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"
|
"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"
|
"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"
|
msgid "Login failed, please check your information"
|
||||||
msgstr "La connexion a échoué, veuillez vérifier vos informations."
|
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}"
|
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}"
|
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}'"
|
msgid "Could not connect to the LDAP server '{uri}'"
|
||||||
msgstr "Impossible de se connecter au serveur LDAP '{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}'"
|
msgid "LDAP authentication failed with user '{user}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"L'authentification au serveur LDAP a échoué pour l'utilisateur '{user}'"
|
"L'authentification au serveur LDAP a échoué pour l'utilisateur '{user}'"
|
||||||
|
@ -360,74 +360,78 @@ msgid "Redirect URIs"
|
||||||
msgstr "URIs de redirection"
|
msgstr "URIs de redirection"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:56
|
#: 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"
|
msgid "Grant types"
|
||||||
msgstr "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"
|
msgid "Scope"
|
||||||
msgstr "Scope"
|
msgstr "Scope"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:74
|
#: canaille/oidc/clients.py:79
|
||||||
msgid "Response types"
|
msgid "Response types"
|
||||||
msgstr "Types de réponse"
|
msgstr "Types de réponse"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:80
|
#: canaille/oidc/clients.py:85
|
||||||
msgid "Token Endpoint Auth Method"
|
msgid "Token Endpoint Auth Method"
|
||||||
msgstr "Token Endpoint Auth Method"
|
msgstr "Token Endpoint Auth Method"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:90
|
#: canaille/oidc/clients.py:95
|
||||||
msgid "Token audiences"
|
msgid "Token audiences"
|
||||||
msgstr "Token audiences"
|
msgstr "Token audiences"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:96
|
#: canaille/oidc/clients.py:101
|
||||||
msgid "Logo URI"
|
msgid "Logo URI"
|
||||||
msgstr "URI du logo"
|
msgstr "URI du logo"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:101
|
#: canaille/oidc/clients.py:106
|
||||||
msgid "Terms of service URI"
|
msgid "Terms of service URI"
|
||||||
msgstr "URI des conditions d'utilisation"
|
msgstr "URI des conditions d'utilisation"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:106
|
#: canaille/oidc/clients.py:111
|
||||||
msgid "Policy URI"
|
msgid "Policy URI"
|
||||||
msgstr "URI de la politique de confidentialité"
|
msgstr "URI de la politique de confidentialité"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:111
|
#: canaille/oidc/clients.py:116
|
||||||
msgid "Software ID"
|
msgid "Software ID"
|
||||||
msgstr "ID du logiciel"
|
msgstr "ID du logiciel"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:116
|
#: canaille/oidc/clients.py:121
|
||||||
msgid "Software Version"
|
msgid "Software Version"
|
||||||
msgstr "Version du logiciel"
|
msgstr "Version du logiciel"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:121
|
#: canaille/oidc/clients.py:126
|
||||||
msgid "JWK"
|
msgid "JWK"
|
||||||
msgstr "JWK"
|
msgstr "JWK"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:126
|
#: canaille/oidc/clients.py:131
|
||||||
msgid "JKW URI"
|
msgid "JKW URI"
|
||||||
msgstr "URI du JWK"
|
msgstr "URI du JWK"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:131
|
#: canaille/oidc/clients.py:136
|
||||||
msgid "Pre-consent"
|
msgid "Pre-consent"
|
||||||
msgstr "Pré-autorisé"
|
msgstr "Pré-autorisé"
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:149
|
#: canaille/oidc/clients.py:154
|
||||||
msgid "The client has not been added. Please check your information."
|
msgid "The client has not been added. Please check your information."
|
||||||
msgstr "Le client n'a pas été ajouté. Veuillez vérifier vos informations."
|
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."
|
msgid "The client has been created."
|
||||||
msgstr "Le client a été créé."
|
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."
|
msgid "The client has not been edited. Please check your information."
|
||||||
msgstr "Le client n'a pas été édité. Veuillez vérifier vos informations."
|
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."
|
msgid "The client has been edited."
|
||||||
msgstr "Le client a été édité."
|
msgstr "Le client a été édité."
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:264
|
#: canaille/oidc/clients.py:276
|
||||||
msgid "The client has been deleted."
|
msgid "The client has been deleted."
|
||||||
msgstr "Le client a été supprimé."
|
msgstr "Le client a été supprimé."
|
||||||
|
|
||||||
|
@ -439,30 +443,43 @@ msgstr "Impossible de supprimer cet accès."
|
||||||
msgid "The access has been revoked"
|
msgid "The access has been revoked"
|
||||||
msgstr "L'accès a été révoqué."
|
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."
|
msgid "Personnal information about yourself, such as your name or your gender."
|
||||||
msgstr "Vos informations personnelles, comme votre nom ou votre genre."
|
msgstr "Vos informations personnelles, comme votre nom ou votre genre."
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:39
|
#: canaille/oidc/oauth.py:45
|
||||||
msgid "Your email address."
|
msgid "Your email address."
|
||||||
msgstr "Votre adresse email."
|
msgstr "Votre adresse email."
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:40
|
#: canaille/oidc/oauth.py:46
|
||||||
msgid "Your postal address."
|
msgid "Your postal address."
|
||||||
msgstr "Votre adresse postale."
|
msgstr "Votre adresse postale."
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:41
|
#: canaille/oidc/oauth.py:47
|
||||||
msgid "Your phone number."
|
msgid "Your phone number."
|
||||||
msgstr "Votre numéro de téléphone."
|
msgstr "Votre numéro de téléphone."
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:42
|
#: canaille/oidc/oauth.py:48
|
||||||
msgid "Groups you are belonging to"
|
msgid "Groups you are belonging to"
|
||||||
msgstr "Les groupes auxquels vous appartenez"
|
msgstr "Les groupes auxquels vous appartenez"
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:125
|
#: canaille/oidc/oauth.py:136
|
||||||
msgid "You have been successfully logged out."
|
msgid "You have been successfully logged out."
|
||||||
msgstr "Vous avez été déconnecté·e."
|
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
|
#: canaille/templates/about.html:12 canaille/themes/default/base.html:104
|
||||||
msgid "About canaille"
|
msgid "About canaille"
|
||||||
msgstr "À propos de canaille"
|
msgstr "À propos de canaille"
|
||||||
|
@ -1242,6 +1259,33 @@ msgid "You did not authorize applications yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous n'avez pas encore autorisé d'application à accéder à votre profil."
|
"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
|
#: canaille/themes/default/base.html:10
|
||||||
msgid "authorization interface"
|
msgid "authorization interface"
|
||||||
msgstr " - Interface de gestion des autorisations"
|
msgstr " - Interface de gestion des autorisations"
|
||||||
|
@ -1266,10 +1310,6 @@ msgstr "Codes"
|
||||||
msgid "Emails"
|
msgid "Emails"
|
||||||
msgstr "Courriels"
|
msgstr "Courriels"
|
||||||
|
|
||||||
#: canaille/themes/default/base.html:90
|
|
||||||
msgid "Log out"
|
|
||||||
msgstr "Déconnexion"
|
|
||||||
|
|
||||||
#~ msgid "Logged in as"
|
#~ msgid "Logged in as"
|
||||||
#~ msgstr "Connecté en tant que"
|
#~ msgstr "Connecté en tant que"
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -17,7 +17,7 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.10.1\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"
|
msgid "Login failed, please check your information"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -342,74 +342,78 @@ msgid "Redirect URIs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:56
|
#: canaille/oidc/clients.py:56
|
||||||
|
msgid "Post logout redirect URIs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: canaille/oidc/clients.py:61
|
||||||
msgid "Grant types"
|
msgid "Grant types"
|
||||||
msgstr ""
|
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"
|
msgid "Scope"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:74
|
#: canaille/oidc/clients.py:79
|
||||||
msgid "Response types"
|
msgid "Response types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:80
|
#: canaille/oidc/clients.py:85
|
||||||
msgid "Token Endpoint Auth Method"
|
msgid "Token Endpoint Auth Method"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:90
|
#: canaille/oidc/clients.py:95
|
||||||
msgid "Token audiences"
|
msgid "Token audiences"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:96
|
#: canaille/oidc/clients.py:101
|
||||||
msgid "Logo URI"
|
msgid "Logo URI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:101
|
#: canaille/oidc/clients.py:106
|
||||||
msgid "Terms of service URI"
|
msgid "Terms of service URI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:106
|
#: canaille/oidc/clients.py:111
|
||||||
msgid "Policy URI"
|
msgid "Policy URI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:111
|
#: canaille/oidc/clients.py:116
|
||||||
msgid "Software ID"
|
msgid "Software ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:116
|
#: canaille/oidc/clients.py:121
|
||||||
msgid "Software Version"
|
msgid "Software Version"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:121
|
#: canaille/oidc/clients.py:126
|
||||||
msgid "JWK"
|
msgid "JWK"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:126
|
#: canaille/oidc/clients.py:131
|
||||||
msgid "JKW URI"
|
msgid "JKW URI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:131
|
#: canaille/oidc/clients.py:136
|
||||||
msgid "Pre-consent"
|
msgid "Pre-consent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:149
|
#: canaille/oidc/clients.py:154
|
||||||
msgid "The client has not been added. Please check your information."
|
msgid "The client has not been added. Please check your information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:184
|
#: canaille/oidc/clients.py:190
|
||||||
msgid "The client has been created."
|
msgid "The client has been created."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:222
|
#: canaille/oidc/clients.py:233
|
||||||
msgid "The client has not been edited. Please check your information."
|
msgid "The client has not been edited. Please check your information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:248
|
#: canaille/oidc/clients.py:260
|
||||||
msgid "The client has been edited."
|
msgid "The client has been edited."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/clients.py:264
|
#: canaille/oidc/clients.py:276
|
||||||
msgid "The client has been deleted."
|
msgid "The client has been deleted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -421,30 +425,42 @@ msgstr ""
|
||||||
msgid "The access has been revoked"
|
msgid "The access has been revoked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:37
|
#: canaille/oidc/oauth.py:43
|
||||||
msgid "Personnal information about yourself, such as your name or your gender."
|
msgid "Personnal information about yourself, such as your name or your gender."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:39
|
#: canaille/oidc/oauth.py:45
|
||||||
msgid "Your email address."
|
msgid "Your email address."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:40
|
#: canaille/oidc/oauth.py:46
|
||||||
msgid "Your postal address."
|
msgid "Your postal address."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:41
|
#: canaille/oidc/oauth.py:47
|
||||||
msgid "Your phone number."
|
msgid "Your phone number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:42
|
#: canaille/oidc/oauth.py:48
|
||||||
msgid "Groups you are belonging to"
|
msgid "Groups you are belonging to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: canaille/oidc/oauth.py:125
|
#: canaille/oidc/oauth.py:136
|
||||||
msgid "You have been successfully logged out."
|
msgid "You have been successfully logged out."
|
||||||
msgstr ""
|
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
|
#: canaille/templates/about.html:12 canaille/themes/default/base.html:104
|
||||||
msgid "About canaille"
|
msgid "About canaille"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1151,6 +1167,33 @@ msgstr ""
|
||||||
msgid "You did not authorize applications yet."
|
msgid "You did not authorize applications yet."
|
||||||
msgstr ""
|
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
|
#: canaille/themes/default/base.html:10
|
||||||
msgid "authorization interface"
|
msgid "authorization interface"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1174,7 +1217,3 @@ msgstr ""
|
||||||
#: canaille/themes/default/base.html:83
|
#: canaille/themes/default/base.html:83
|
||||||
msgid "Emails"
|
msgid "Emails"
|
||||||
msgstr ""
|
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.integrations.flask_client import OAuth
|
||||||
from authlib.oidc.discovery import get_well_known_url
|
from authlib.oidc.discovery import get_well_known_url
|
||||||
|
from flask import current_app
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
|
@ -39,6 +43,7 @@ def create_app():
|
||||||
def authorize():
|
def authorize():
|
||||||
token = oauth.canaille.authorize_access_token()
|
token = oauth.canaille.authorize_access_token()
|
||||||
session["user"] = token.get("userinfo")
|
session["user"] = token.get("userinfo")
|
||||||
|
session["id_token"] = token["id_token"]
|
||||||
flash("You have been successfully logged in.", "success")
|
flash("You have been successfully logged in.", "success")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
@ -49,7 +54,31 @@ def create_app():
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
flash("You have been successfully logged out.", "success")
|
flash("You have been successfully logged out", "success")
|
||||||
return redirect(url_for("index"))
|
|
||||||
|
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
|
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
|
oauthClientContact: admin@mydomain.tld
|
||||||
oauthClientURI: http://localhost:5001
|
oauthClientURI: http://localhost:5001
|
||||||
oauthRedirectURIs: http://localhost:5001/authorize
|
oauthRedirectURIs: http://localhost:5001/authorize
|
||||||
|
oauthPostLogoutRedirectURI: http://localhost:5001/
|
||||||
oauthGrantType: authorization_code
|
oauthGrantType: authorization_code
|
||||||
oauthGrantType: refresh_token
|
oauthGrantType: refresh_token
|
||||||
oauthScope: openid
|
oauthScope: openid
|
||||||
|
@ -98,6 +99,7 @@ oauthClientName: Client2
|
||||||
oauthClientContact: admin@mydomain.tld
|
oauthClientContact: admin@mydomain.tld
|
||||||
oauthClientURI: http://localhost:5002
|
oauthClientURI: http://localhost:5002
|
||||||
oauthRedirectURIs: http://localhost:5002/authorize
|
oauthRedirectURIs: http://localhost:5002/authorize
|
||||||
|
oauthPostLogoutRedirectURI: http://localhost:5002/
|
||||||
oauthGrantType: authorization_code
|
oauthGrantType: authorization_code
|
||||||
oauthGrantType: refresh_token
|
oauthGrantType: refresh_token
|
||||||
oauthScope: openid
|
oauthScope: openid
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from authlib.oidc.core.grants.util import generate_id_token
|
||||||
from canaille.oidc.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from canaille.oidc.models import Client
|
from canaille.oidc.models import Client
|
||||||
from canaille.oidc.models import Consent
|
from canaille.oidc.models import Consent
|
||||||
from canaille.oidc.models import Token
|
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
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +38,7 @@ def client(testclient, other_client, slapd_connection):
|
||||||
policy_uri="https://mydomain.tld/policy",
|
policy_uri="https://mydomain.tld/policy",
|
||||||
jwk_uri="https://mydomain.tld/jwk",
|
jwk_uri="https://mydomain.tld/jwk",
|
||||||
token_endpoint_auth_method="client_secret_basic",
|
token_endpoint_auth_method="client_secret_basic",
|
||||||
|
post_logout_redirect_uris=["https://mydomain.tld/disconnected"],
|
||||||
)
|
)
|
||||||
c.audience = [c.dn, other_client.dn]
|
c.audience = [c.dn, other_client.dn]
|
||||||
c.save()
|
c.save()
|
||||||
|
@ -73,6 +77,7 @@ def other_client(testclient, slapd_connection):
|
||||||
policy_uri="https://myotherdomain.tld/policy",
|
policy_uri="https://myotherdomain.tld/policy",
|
||||||
jwk_uri="https://myotherdomain.tld/jwk",
|
jwk_uri="https://myotherdomain.tld/jwk",
|
||||||
token_endpoint_auth_method="client_secret_basic",
|
token_endpoint_auth_method="client_secret_basic",
|
||||||
|
post_logout_redirect_uris=["https://myotherdomain.tld/disconnected"],
|
||||||
)
|
)
|
||||||
c.audience = [c.dn]
|
c.audience = [c.dn]
|
||||||
c.save()
|
c.save()
|
||||||
|
@ -131,6 +136,16 @@ def token(testclient, client, user, slapd_connection):
|
||||||
pass
|
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
|
@pytest.fixture
|
||||||
def consent(testclient, client, user, slapd_connection):
|
def consent(testclient, client, user, slapd_connection):
|
||||||
t = Consent(
|
t = Consent(
|
||||||
|
|
|
@ -38,6 +38,7 @@ def test_client_add(testclient, logged_admin):
|
||||||
"jwk_uri": "https://foo.bar/jwks.json",
|
"jwk_uri": "https://foo.bar/jwks.json",
|
||||||
"audience": [],
|
"audience": [],
|
||||||
"preconsent": False,
|
"preconsent": False,
|
||||||
|
"post_logout_redirect_uris": ["https://foo.bar/disconnected"],
|
||||||
}
|
}
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
res.form[k].force_value(v)
|
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",
|
"jwk_uri": "https://foo.bar/jwks.json",
|
||||||
"audience": [client.dn, other_client.dn],
|
"audience": [client.dn, other_client.dn],
|
||||||
"preconsent": True,
|
"preconsent": True,
|
||||||
|
"post_logout_redirect_uris": ["https://foo.bar/disconnected"],
|
||||||
}
|
}
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
res.forms["clientadd"][k].force_value(v)
|
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