Use a unique identifier to indentify users in URLS

Previously we used the uid since we supposed this value was always
valid, but some users user the mail attribute as the User RDN in their
OpenLDAP installation, and do not have a uuid.
This commit is contained in:
Éloi Rivard 2023-06-27 17:41:00 +02:00
parent 4551dc3f60
commit 57af18d557
22 changed files with 143 additions and 125 deletions

View file

@ -85,6 +85,10 @@ class User(canaille.core.models.User, LDAPObject):
return user
@property
def identifier(self):
return self.rdn_value
def has_password(self):
return bool(self.password)

View file

@ -60,7 +60,7 @@ def index():
if user.can_edit_self or user.can_manage_users:
return redirect(
url_for("account.profile_edition", username=current_user().user_name[0])
url_for("account.profile_edition", identifier=current_user().identifier)
)
if user.can_use_oidc:
@ -82,7 +82,7 @@ def about():
def login():
if current_user():
return redirect(
url_for("account.profile_edition", username=current_user().user_name[0])
url_for("account.profile_edition", identifier=current_user().identifier)
)
form = LoginForm(request.form or None)
@ -93,7 +93,7 @@ def login():
user = models.User.get_from_login(form.login.data)
if user and not user.has_password():
return redirect(url_for("account.firstlogin", user_name=user.user_name[0]))
return redirect(url_for("account.firstlogin", identifier=user.identifier))
if not form.validate():
models.User.logout()
@ -119,7 +119,7 @@ def password():
user = models.User.get_from_login(session["attempt_login"])
if user and not user.has_password():
return redirect(url_for("account.firstlogin", user_name=user.user_name[0]))
return redirect(url_for("account.firstlogin", identifier=user.identifier))
if not form.validate() or not user:
models.User.logout()
@ -160,15 +160,15 @@ def logout():
return redirect("/")
@bp.route("/firstlogin/<user_name>", methods=("GET", "POST"))
def firstlogin(user_name):
user = models.User.get_from_login(user_name)
@bp.route("/firstlogin/<identifier>", methods=("GET", "POST"))
def firstlogin(identifier):
user = models.User.get_from_login(identifier)
if not user or user.has_password():
abort(404)
form = FirstLoginForm(request.form or None)
if not request.form:
return render_template("firstlogin.html", form=form, user_name=user_name)
return render_template("firstlogin.html", form=form, identifier=identifier)
form.validate()
@ -182,7 +182,7 @@ def firstlogin(user_name):
else:
flash(_("Could not send the password initialization email"), "error")
return render_template("firstlogin.html", form=form, user_name=user_name)
return render_template("firstlogin.html", form=form, identifier=identifier)
@bp.route("/users", methods=["GET", "POST"])
@ -360,7 +360,7 @@ def registration(data, hash):
user = profile_create(current_app, form)
user.login()
flash(_("Your account has been created successfully."), "success")
return redirect(url_for("account.profile_edition", username=user.user_name[0]))
return redirect(url_for("account.profile_edition", identifier=user.identifier))
@bp.route("/profile", methods=("GET", "POST"))
@ -393,7 +393,7 @@ def profile_creation(user):
)
user = profile_create(current_app, form)
return redirect(url_for("account.profile_edition", username=user.user_name[0]))
return redirect(url_for("account.profile_edition", identifier=user.identifier))
def profile_create(current_app, form):
@ -424,19 +424,19 @@ def profile_create(current_app, form):
return user
@bp.route("/profile/<username>", methods=("GET", "POST"))
@bp.route("/profile/<identifier>", methods=("GET", "POST"))
@user_needed()
def profile_edition(user, username):
def profile_edition(user, identifier):
editor = user
if not user.can_manage_users and not (
user.can_edit_self and username == user.user_name[0]
user.can_edit_self and identifier == user.identifier
):
abort(403)
menuitem = "profile" if username == editor.user_name[0] else "users"
menuitem = "profile" if identifier == editor.identifier else "users"
fields = editor.read | editor.write
if username != editor.user_name[0]:
user = models.User.get_from_login(username)
if identifier != editor.identifier:
user = models.User.get_from_login(identifier)
else:
user = editor
@ -524,18 +524,18 @@ def profile_edition(user, username):
user.save()
flash(_("Profile updated successfully."), "success")
return redirect(url_for("account.profile_edition", username=username))
return redirect(url_for("account.profile_edition", identifier=identifier))
@bp.route("/profile/<username>/settings", methods=("GET", "POST"))
@bp.route("/profile/<identifier>/settings", methods=("GET", "POST"))
@user_needed()
def profile_settings(user, username):
def profile_settings(user, identifier):
if not user.can_manage_users and not (
user.can_edit_self and username == user.user_name[0]
user.can_edit_self and identifier == user.identifier
):
abort(403)
edited_user = models.User.get_from_login(username)
edited_user = models.User.get_from_login(identifier)
if not edited_user:
abort(404)
@ -640,7 +640,7 @@ def profile_settings_edit(editor, edited_user):
edited_user.save()
flash(_("Profile updated successfully."), "success")
return redirect(
url_for("account.profile_settings", username=edited_user.user_name[0])
url_for("account.profile_settings", identifier=edited_user.identifier)
)
return render_template(
@ -671,10 +671,10 @@ def profile_delete(user, edited_user):
return redirect(url_for("account.users"))
@bp.route("/impersonate/<username>")
@bp.route("/impersonate/<identifier>")
@permissions_needed("impersonate_users")
def impersonate(user, username):
puppet = models.User.get_from_login(username)
def impersonate(user, identifier):
puppet = models.User.get_from_login(identifier)
if not puppet:
abort(404)
@ -735,16 +735,16 @@ def forgotten():
return render_template("forgotten-password.html", form=form)
@bp.route("/reset/<user_name>/<hash>", methods=["GET", "POST"])
def reset(user_name, hash):
@bp.route("/reset/<identifier>/<hash>", methods=["GET", "POST"])
def reset(identifier, hash):
if not current_app.config.get("ENABLE_PASSWORD_RECOVERY", True):
abort(404)
form = PasswordResetForm(request.form)
user = models.User.get_from_login(user_name)
user = models.User.get_from_login(identifier)
if not user or hash != profile_hash(
user.user_name[0],
user.identifier,
user.preferred_email,
user.password[0] if user.has_password() else "",
):
@ -759,19 +759,19 @@ def reset(user_name, hash):
user.login()
flash(_("Your password has been updated successfully"), "success")
return redirect(url_for("account.profile_edition", username=user_name))
return redirect(url_for("account.profile_edition", identifier=identifier))
return render_template(
"reset-password.html", form=form, user_name=user_name, hash=hash
"reset-password.html", form=form, identifier=identifier, hash=hash
)
@bp.route("/profile/<user_name>/<field>")
def photo(user_name, field):
@bp.route("/profile/<identifier>/<field>")
def photo(identifier, field):
if field.lower() != "photo":
abort(404)
user = models.User.get_from_login(user_name)
user = models.User.get_from_login(identifier)
if not user:
abort(404)
@ -779,7 +779,7 @@ def photo(user_name, field):
if request.if_modified_since and request.if_modified_since >= user.last_modified:
return "", 304
etag = profile_hash(user_name, user.last_modified.isoformat())
etag = profile_hash(identifier, user.last_modified.isoformat())
if request.if_none_match and etag in request.if_none_match:
return "", 304

View file

@ -75,8 +75,8 @@ def password_init_html(user):
base_url = url_for("account.index", _external=True)
reset_url = url_for(
"account.reset",
user_name=user.user_name[0],
hash=profile_hash(user.user_name[0], user.preferred_email, user.password[0]),
identifier=user.identifier,
hash=profile_hash(user.identifier, user.preferred_email, user.password[0]),
_external=True,
)
@ -98,8 +98,8 @@ def password_init_txt(user):
base_url = url_for("account.index", _external=True)
reset_url = url_for(
"account.reset",
user_name=user.user_name[0],
hash=profile_hash(user.user_name[0], user.preferred_email, user.password[0]),
identifier=user.identifier,
hash=profile_hash(user.identifier, user.preferred_email, user.password[0]),
_external=True,
)
@ -117,8 +117,8 @@ def password_reset_html(user):
base_url = url_for("account.index", _external=True)
reset_url = url_for(
"account.reset",
user_name=user.user_name[0],
hash=profile_hash(user.user_name[0], user.preferred_email, user.password[0]),
identifier=user.identifier,
hash=profile_hash(user.identifier, user.preferred_email, user.password[0]),
_external=True,
)
@ -140,8 +140,8 @@ def password_reset_txt(user):
base_url = url_for("account.index", _external=True)
reset_url = url_for(
"account.reset",
user_name=user.user_name[0],
hash=profile_hash(user.user_name[0], user.preferred_email, user.password[0]),
identifier=user.identifier,
hash=profile_hash(user.identifier, user.preferred_email, user.password[0]),
_external=True,
)
@ -153,14 +153,14 @@ def password_reset_txt(user):
)
@bp.route("/mail/<user_name>/<email>/invitation.html")
@bp.route("/mail/<identifier>/<email>/invitation.html")
@permissions_needed("manage_oidc")
def invitation_html(user, user_name, email):
def invitation_html(user, identifier, email):
base_url = url_for("account.index", _external=True)
registration_url = url_for(
"account.registration",
data=obj_to_b64([user_name, email]),
hash=profile_hash(user_name, email),
data=obj_to_b64([identifier, email]),
hash=profile_hash(identifier, email),
_external=True,
)
@ -176,14 +176,14 @@ def invitation_html(user, user_name, email):
)
@bp.route("/mail/<user_name>/<email>/invitation.txt")
@bp.route("/mail/<identifier>/<email>/invitation.txt")
@permissions_needed("manage_oidc")
def invitation_txt(user, user_name, email):
def invitation_txt(user, identifier, email):
base_url = url_for("account.index", _external=True)
registration_url = url_for(
"account.registration",
data=obj_to_b64([user_name, email]),
hash=profile_hash(user_name, email),
data=obj_to_b64([identifier, email]),
hash=profile_hash(identifier, email),
_external=True,
)

View file

@ -39,9 +39,9 @@ def send_password_reset_mail(user):
base_url = url_for("account.index", _external=True)
reset_url = url_for(
"account.reset",
user_name=user.user_name[0],
identifier=user.identifier,
hash=profile_hash(
user.user_name[0],
user.identifier,
user.preferred_email,
user.password[0] if user.has_password() else "",
),
@ -79,9 +79,9 @@ def send_password_initialization_mail(user):
base_url = url_for("account.index", _external=True)
reset_url = url_for(
"account.reset",
user_name=user.user_name[0],
identifier=user.identifier,
hash=profile_hash(
user.user_name[0],
user.identifier,
user.preferred_email,
user.password[0] if user.has_password() else "",
),

View file

@ -34,6 +34,14 @@ class User:
except (IndexError, KeyError):
pass
@property
def identifier(self):
"""
Returns a unique value that will be used to identify the user.
This value will be used in URLs in canaille, so it should be unique and short.
"""
raise NotImplementedError()
def has_password(self):
raise NotImplementedError()

View file

@ -101,8 +101,8 @@
<div class="item">
<div class="right floated content">
<div class="ui buttons">
<a class="ui button primary" href="{{ url_for("admin.invitation_txt", user_name=user.user_name, email=user.preferred_email) }}">TXT</a>
<a class="ui button primary" href="{{ url_for("admin.invitation_html", user_name=user.user_name, email=user.preferred_email) }}">HTML</a>
<a class="ui button primary" href="{{ url_for("admin.invitation_txt", identifier=user.identifier, email=user.preferred_email) }}">TXT</a>
<a class="ui button primary" href="{{ url_for("admin.invitation_html", identifier=user.identifier, email=user.preferred_email) }}">HTML</a>
</div>
</div>
<div class="middle aligned content">

View file

@ -70,7 +70,7 @@
<tr>
<td>{{ _("Subject") }}</td>
<td>
<a href="{{ url_for("account.profile_edition", username=token.subject.user_name[0]) }}">
<a href="{{ url_for("account.profile_edition", identifier=token.subject.identifier) }}">
{{ token.subject.name }} - {{ token.subject.user_name[0] }}
</a>
</td>

View file

@ -11,7 +11,7 @@
</h2>
<div class="ui message">
<p>
{{ _("You are currently logged in as %(username)s.", username=user.formatted_name[0]) }}
{{ _("You are currently logged in as %(username)s.", username=user.identifier) }}
{% if client %}
{{ _("The application %(client_name)s wants to disconnect your account.", client_name=client.client_name) }}
{% endif %}

View file

@ -14,7 +14,7 @@
<td><a href="{{ url_for('oidc.authorizations.view', authorization_id=authorization.authorization_code_id) }}">{{ authorization.authorization_code_id }}</a></td>
<td><a href="{{ url_for('oidc.clients.edit', client_id=authorization.client.client_id) }}">{{ authorization.client.client_id }}</a></td>
<td>
<a href="{{ url_for("account.profile_edition", username=authorization.subject.user_name[0]) }}">
<a href="{{ url_for("account.profile_edition", identifier=authorization.subject.identifier) }}">
{{ authorization.subject.user_name[0] }}
</a>
</td>

View file

@ -22,7 +22,7 @@
</a>
</td>
<td>
<a href="{{ url_for("account.profile_edition", username=token.subject.user_name[0]) }}">
<a href="{{ url_for("account.profile_edition", identifier=token.subject.identifier) }}">
{{ token.subject.user_name[0] }}
</a>
</td>

View file

@ -24,7 +24,7 @@
<tr>
{% if user.can_read("photo") %}
<td>
<a href="{{ url_for('account.profile_edition', username=watched_user.user_name[0]) }}">
<a href="{{ url_for('account.profile_edition', identifier=watched_user.identifier) }}">
{% if user.can_manage_users and watched_user.locked %}
<i class="lock circle big black icon" title="{% trans %}This account is locked{% endtrans %}"></i>
{% elif watched_user.photo and watched_user.photo[0] %}
@ -36,7 +36,7 @@
</td>
{% endif %}
{% if user.can_read("user_name") %}
<td><a href="{{ url_for('account.profile_edition', username=watched_user.user_name[0]) }}">{{ watched_user.user_name[0] }}</a></td>
<td><a href="{{ url_for('account.profile_edition', identifier=watched_user.identifier) }}">{{ watched_user.user_name[0] }}</a></td>
{% endif %}
{% if user.can_read("family_name") or user.can_read("given_name") %}
<td>{{ watched_user.formatted_name[0] }}</td>

View file

@ -18,11 +18,11 @@
{% block submenu %}
<nav class="ui bottom attached two item borderless menu">
<a class="active item" href="{{ url_for('account.profile_edition', username=edited_user.user_name[0]) }}">
<a class="active item" href="{{ url_for('account.profile_edition', identifier=edited_user.identifier) }}">
<i class="id card icon"></i>
{% trans %}Personal information{% endtrans %}
</a>
<a class="item" href="{{ url_for('account.profile_settings', username=edited_user.user_name[0]) }}">
<a class="item" href="{{ url_for('account.profile_settings', identifier=edited_user.identifier) }}">
<i class="tools icon"></i>
{% trans %}Account information{% endtrans %}
</a>
@ -66,7 +66,7 @@
<a class="ui right corner label photo-delete-icon" title="{{ _("Delete the photo") }}">
<i class="times icon"></i>
</a>
<img src="{% if photo %}{{ url_for("account.photo", user_name=edited_user.user_name[0], field="photo") }}{% endif %}" alt="User photo">
<img src="{% if photo %}{{ url_for("account.photo", identifier=edited_user.identifier, field="photo") }}{% endif %}" alt="User photo">
</label>
<label
class="ui centered photo-placeholder"

View file

@ -15,11 +15,11 @@
{% block submenu %}
<nav class="ui bottom attached two item borderless menu">
<a class="item" href="{{ url_for('account.profile_edition', username=edited_user.user_name[0]) }}">
<a class="item" href="{{ url_for('account.profile_edition', identifier=edited_user.identifier) }}">
<i class="id card icon"></i>
{% trans %}Personal information{% endtrans %}
</a>
<a class="active item" href="{{ url_for('account.profile_settings', username=edited_user.user_name[0]) }}">
<a class="active item" href="{{ url_for('account.profile_settings', identifier=edited_user.identifier) }}">
<i class="tools icon"></i>
{% trans %}Account information{% endtrans %}
</a>
@ -188,8 +188,8 @@
</button>
{% endif %}
{% if user.can_impersonate_users and user.user_name != edited_user.user_name %}
<a href="{{ url_for('account.impersonate', username=edited_user.user_name[0]) }}" class="ui right floated basic button" name="action" value="impersonate" id="impersonate" hx-boost="false">
{% if user.can_impersonate_users and user.identifier != edited_user.identifier %}
<a href="{{ url_for('account.impersonate', identifier=edited_user.identifier) }}" class="ui right floated basic button" name="action" value="impersonate" id="impersonate" hx-boost="false">
{{ _("Impersonate") }}
</a>
{% endif %}

View file

@ -13,7 +13,7 @@
</h3>
<div class="ui attached clearing segment">
{{ fui.render_form(form, _("Password reset"), action=url_for("account.reset", user_name=user_name, hash=hash)) }}
{{ fui.render_form(form, _("Password reset"), action=url_for("account.reset", identifier=identifier, hash=hash)) }}
</div>
</div>
{% endblock %}

View file

@ -31,7 +31,7 @@
{% if user.can_edit_self %}
<a class="item {% if menuitem == "profile" %}active{% endif %}"
href="{{ url_for('account.profile_edition', username=user.user_name[0]) }}">
href="{{ url_for('account.profile_edition', identifier=user.identifier) }}">
<i class="id card icon"></i>
{% trans %}Profile{% endtrans %}
</a>

View file

@ -77,7 +77,7 @@ USER_BASE = "ou=users,dc=mydomain,dc=tld"
# USER_CLASS = "inetOrgPerson"
# The attribute to identify an object in the User dn.
USER_ID_ATTRIBUTE = "uid"
# USER_ID_ATTRIBUTE = "uid"
# Filter to match users on sign in. Supports a variable
# {login} that can be used to compare against several fields:

View file

@ -28,7 +28,7 @@
</div>
{% endif %}
<a class="item {% if menuitem == "profile" %}active{% endif %}"
href="{{ url_for('account.profile_edition', username=user.user_name[0]) }}">
href="{{ url_for('account.profile_edition', identifier=user.identifier) }}">
<i class="id card icon"></i>
{% trans %}My profile{% endtrans %}
</a>

View file

@ -34,6 +34,7 @@ def ldap_configuration(configuration, slapd_server):
"BIND_DN": slapd_server.root_dn,
"BIND_PW": slapd_server.root_pw,
"USER_BASE": "ou=users",
"USER_ID_ATTRIBUTE": "uid",
"USER_FILTER": "(uid={login})",
"GROUP_BASE": "ou=groups",
"TIMEOUT": 0.1,

View file

@ -33,7 +33,7 @@ def test_object_creation(app, backend):
def test_repr(backend, foo_group, user):
assert repr(foo_group) == "<Group display_name=foo>"
assert repr(user) == "<User formatted_name=John (johnny) Doe>"
assert repr(user) == "<User user_name=user>"
def test_dn_when_leading_space_in_id_attribute(backend):
@ -47,7 +47,7 @@ def test_dn_when_leading_space_in_id_attribute(backend):
assert ldap.dn.is_dn(user.dn)
assert ldap.dn.dn2str(ldap.dn.str2dn(user.dn)) == user.dn
assert user.dn == "cn=Doe,ou=users,dc=mydomain,dc=tld"
assert user.dn == "uid=user,ou=users,dc=mydomain,dc=tld"
user.delete()
@ -56,14 +56,14 @@ def test_dn_when_ldap_special_char_in_id_attribute(backend):
user = models.User(
formatted_name="#Doe", # special char
family_name="Doe",
user_name="user",
user_name="#user",
emails="john@doe.com",
)
user.save()
assert ldap.dn.is_dn(user.dn)
assert ldap.dn.dn2str(ldap.dn.str2dn(user.dn)) == user.dn
assert user.dn == "cn=\\#Doe,ou=users,dc=mydomain,dc=tld"
assert user.dn == "uid=\\#user,ou=users,dc=mydomain,dc=tld"
user.delete()
@ -182,7 +182,7 @@ def test_object_class_update(backend, testclient):
testclient.app.config["BACKENDS"]["LDAP"]["USER_CLASS"] = ["inetOrgPerson"]
setup_ldap_models(testclient.app.config)
user1 = models.User(cn="foo1", sn="bar1")
user1 = models.User(cn="foo1", sn="bar1", user_name="baz1")
user1.save()
assert user1.objectClass == ["inetOrgPerson"]
@ -194,7 +194,7 @@ def test_object_class_update(backend, testclient):
]
setup_ldap_models(testclient.app.config)
user2 = models.User(cn="foo2", sn="bar2")
user2 = models.User(cn="foo2", sn="bar2", user_name="baz2")
user2.save()
assert user2.objectClass == ["inetOrgPerson", "extensibleObject"]

View file

@ -180,7 +180,9 @@ def test_model_references_set_unsaved_object(
group.save()
user.reload() # an LDAP group can be inconsistent by containing members which doesn't exist
non_existent_user = models.User(formatted_name="foo", family_name="bar")
non_existent_user = models.User(
formatted_name="foo", family_name="bar", user_name="baz"
)
group.members = group.members + [non_existent_user]
assert group.members == [user, non_existent_user]

View file

@ -18,6 +18,9 @@ def test_required_methods(testclient):
with pytest.raises(NotImplementedError):
user.set_password("password")
with pytest.raises(NotImplementedError):
user.identifier
def test_user_get_from_login(testclient, user, backend):
assert models.User.get_from_login(login="invalid") is None

View file

@ -4,14 +4,14 @@ from canaille.oidc.oauth import get_jwt_config
def test_end_session(testclient, backend, logged_user, client, id_token):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -24,18 +24,18 @@ def test_end_session(testclient, backend, logged_user, client, id_token):
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_end_session_no_client_id(testclient, backend, logged_user, client, id_token):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
},
@ -47,19 +47,19 @@ def test_end_session_no_client_id(testclient, backend, logged_user, client, id_t
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_no_redirect_uri_no_redirect(
testclient, backend, logged_user, client, id_token
):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
res = testclient.get(
"/oauth/end_session",
params={
"id_token_hint": id_token,
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"state": "foobar",
},
@ -71,20 +71,20 @@ def test_no_redirect_uri_no_redirect(
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_bad_redirect_uri_no_redirect(
testclient, backend, logged_user, client, id_token
):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -97,17 +97,17 @@ def test_bad_redirect_uri_no_redirect(
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_no_client_hint_no_redirect(testclient, backend, logged_user, client, id_token):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
post_logout_redirect_url = "https://mydomain.tld/disconnected"
res = testclient.get(
"/oauth/end_session",
params={
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
},
@ -121,17 +121,17 @@ def test_no_client_hint_no_redirect(testclient, backend, logged_user, client, id
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_end_session_invalid_client_id(testclient, backend, logged_user, client):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
post_logout_redirect_url = "https://mydomain.tld/disconnected"
res = testclient.get(
"/oauth/end_session",
params={
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"post_logout_redirect_uri": post_logout_redirect_url,
"client_id": "invalid_client_id",
"state": "foobar",
@ -147,7 +147,7 @@ def test_end_session_invalid_client_id(testclient, backend, logged_user, client)
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_client_hint_invalid(testclient, backend, logged_user, client):
@ -158,14 +158,14 @@ def test_client_hint_invalid(testclient, backend, logged_user, client):
**get_jwt_config(None),
)
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
},
@ -177,17 +177,17 @@ def test_client_hint_invalid(testclient, backend, logged_user, client):
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_no_jwt_logout(testclient, backend, logged_user, client):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
post_logout_redirect_url = "https://mydomain.tld/disconnected"
res = testclient.get(
"/oauth/end_session",
params={
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -203,17 +203,17 @@ def test_no_jwt_logout(testclient, backend, logged_user, client):
assert res.location == f"{post_logout_redirect_url}?state=foobar"
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_no_jwt_no_logout(testclient, backend, logged_user, client):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
post_logout_redirect_url = "https://mydomain.tld/disconnected"
res = testclient.get(
"/oauth/end_session",
params={
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -228,20 +228,20 @@ def test_no_jwt_no_logout(testclient, backend, logged_user, client):
assert res.location == "/"
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
def test_jwt_not_issued_here(testclient, backend, logged_user, client, id_token):
testclient.app.config["OIDC"]["JWT"]["ISS"] = "https://foo.bar"
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -263,14 +263,14 @@ def test_client_hint_mismatch(testclient, backend, logged_user, client):
**get_jwt_config(None),
)
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -285,7 +285,7 @@ def test_client_hint_mismatch(testclient, backend, logged_user, client):
def test_bad_user_id_token_mismatch(testclient, backend, logged_user, client, admin):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
id_token = generate_id_token(
{},
@ -299,7 +299,7 @@ def test_bad_user_id_token_mismatch(testclient, backend, logged_user, client, ad
"/oauth/end_session",
params={
"id_token_hint": id_token,
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -315,18 +315,18 @@ def test_bad_user_id_token_mismatch(testclient, backend, logged_user, client, ad
assert res.location == f"{post_logout_redirect_url}?state=foobar"
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_bad_user_hint(testclient, backend, logged_user, client, id_token, admin):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": admin.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -342,17 +342,17 @@ def test_bad_user_hint(testclient, backend, logged_user, client, id_token, admin
assert res.location == f"{post_logout_redirect_url}?state=foobar"
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)
def test_no_jwt_bad_csrf(testclient, backend, logged_user, client):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", status=200)
post_logout_redirect_url = "https://mydomain.tld/disconnected"
res = testclient.get(
"/oauth/end_session",
params={
"logout_hint": logged_user.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -371,7 +371,7 @@ def test_end_session_already_disconnected(testclient, backend, user, client, id_
"/oauth/end_session",
params={
"id_token_hint": id_token,
"logout_hint": user.user_name[0],
"logout_hint": user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
"state": "foobar",
@ -383,14 +383,14 @@ def test_end_session_already_disconnected(testclient, backend, user, client, id_
def test_end_session_no_state(testclient, backend, logged_user, client, id_token):
testclient.get(f"/profile/{logged_user.user_name[0]}", status=200)
testclient.get(f"/profile/{logged_user.identifier}", 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.user_name[0],
"logout_hint": logged_user.identifier,
"client_id": client.client_id,
"post_logout_redirect_uri": post_logout_redirect_url,
},
@ -402,4 +402,4 @@ def test_end_session_no_state(testclient, backend, logged_user, client, id_token
with testclient.session_transaction() as sess:
assert not sess.get("user_id")
testclient.get(f"/profile/{logged_user.user_name[0]}", status=403)
testclient.get(f"/profile/{logged_user.identifier}", status=403)