Implemented a basic WebFinger endpoint.

This commit is contained in:
Éloi Rivard 2022-10-03 17:25:32 +02:00 committed by Éloi Rivard
parent 9100b8fb13
commit e45ad6e21c
7 changed files with 126 additions and 24 deletions

View file

@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_, The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_,
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_. and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
[0.0.12] - 2022-xx-xx
=====================
Added
*****
- Basic WebFinger endpoint. :pr:`59`
[0.0.11] - 2022-08-11 [0.0.11] - 2022-08-11
===================== =====================

View file

@ -1,23 +1,23 @@
{ {
"issuer": "issuer":
"https://mydomain.tld", "https://auth.mydomain.tld",
"authorization_endpoint": "authorization_endpoint":
"https://mydomain.tld/oauth/authorize", "https://auth.mydomain.tld/oauth/authorize",
"token_endpoint": "token_endpoint":
"https://mydomain.tld/oauth/token", "https://auth.mydomain.tld/oauth/token",
"token_endpoint_auth_methods_supported": "token_endpoint_auth_methods_supported":
["client_secret_basic", "private_key_jwt", ["client_secret_basic", "private_key_jwt",
"client_secret_post", "none"], "client_secret_post", "none"],
"token_endpoint_auth_signing_alg_values_supported": "token_endpoint_auth_signing_alg_values_supported":
["RS256", "ES256"], ["RS256", "ES256"],
"userinfo_endpoint": "userinfo_endpoint":
"https://mydomain.tld/oauth/userinfo", "https://auth.mydomain.tld/oauth/userinfo",
"introspection_endpoint": "introspection_endpoint":
"https://mydomain.tld/oauth/introspect", "https://auth.mydomain.tld/oauth/introspect",
"jwks_uri": "jwks_uri":
"https://mydomain.tld/oauth/jwks.json", "https://auth.mydomain.tld/oauth/jwks.json",
"registration_endpoint": "registration_endpoint":
"https://mydomain.tld/oauth/register", "https://auth.mydomain.tld/oauth/register",
"scopes_supported": "scopes_supported":
["openid", "profile", "email", "address", ["openid", "profile", "email", "address",
"phone", "groups"], "phone", "groups"],
@ -25,7 +25,7 @@
["code", "token", "id_token", "code token", ["code", "token", "id_token", "code token",
"code id_token", "token id_token"], "code id_token", "token id_token"],
"service_documentation": "service_documentation":
"https://mydomain.tld/documentation.html", "https://auth.mydomain.tld/documentation.html",
"ui_locales_supported": "ui_locales_supported":
["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"] ["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
} }

View file

@ -1,27 +1,27 @@
{ {
"issuer": "issuer":
"https://mydomain.tld", "https://auth.mydomain.tld",
"authorization_endpoint": "authorization_endpoint":
"https://mydomain.tld/oauth/authorize", "https://auth.mydomain.tld/oauth/authorize",
"token_endpoint": "token_endpoint":
"https://mydomain.tld/oauth/token", "https://auth.mydomain.tld/oauth/token",
"token_endpoint_auth_methods_supported": "token_endpoint_auth_methods_supported":
["client_secret_basic", "private_key_jwt", ["client_secret_basic", "private_key_jwt",
"client_secret_post", "none"], "client_secret_post", "none"],
"token_endpoint_auth_signing_alg_values_supported": "token_endpoint_auth_signing_alg_values_supported":
["RS256"], ["RS256"],
"userinfo_endpoint": "userinfo_endpoint":
"https://mydomain.tld/oauth/userinfo", "https://auth.mydomain.tld/oauth/userinfo",
"check_session_iframe": "check_session_iframe":
"https://mydomain.tld/oauth/check_session", "https://auth.mydomain.tld/oauth/check_session",
"end_session_endpoint": "end_session_endpoint":
"https://mydomain.tld/oauth/end_session", "https://auth.mydomain.tld/oauth/end_session",
"jwks_uri": "jwks_uri":
"https://mydomain.tld/oauth/jwks.json", "https://auth.mydomain.tld/oauth/jwks.json",
"registration_endpoint": "registration_endpoint":
"https://mydomain.tld/oauth/register", "https://auth.mydomain.tld/oauth/register",
"introspection_endpoint": "introspection_endpoint":
"https://mydomain.tld/oauth/introspect", "https://auth.mydomain.tld/oauth/introspect",
"scopes_supported": "scopes_supported":
["openid", "profile", "email", "address", ["openid", "profile", "email", "address",
"phone", "groups"], "phone", "groups"],
@ -60,7 +60,7 @@
"claims_parameter_supported": "claims_parameter_supported":
true, true,
"service_documentation": "service_documentation":
"https://mydomain.tld/oauth/service_documentation.html", "https://auth.mydomain.tld/oauth/service_documentation.html",
"ui_locales_supported": "ui_locales_supported":
["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"] ["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
} }

View file

@ -2,19 +2,48 @@ import json
from flask import Blueprint from flask import Blueprint
from flask import current_app from flask import current_app
from flask import g
from flask import jsonify from flask import jsonify
from flask import request
bp = Blueprint("home", __name__, url_prefix="/.well-known") bp = Blueprint("home", __name__, url_prefix="/.well-known")
def cached_oauth_authorization_server():
if "oauth_authorization_server" not in g:
with open(current_app.config["OAUTH2_METADATA_FILE"]) as fd:
g.oauth_authorization_server = json.load(fd)
return g.oauth_authorization_server
def cached_openid_configuration():
if "openid_configuration" not in g:
with open(current_app.config["OIDC_METADATA_FILE"]) as fd:
g.openid_configuration = json.load(fd)
return g.openid_configuration
@bp.route("/oauth-authorization-server") @bp.route("/oauth-authorization-server")
def oauth_authorization_server(): def oauth_authorization_server():
with open(current_app.config["OAUTH2_METADATA_FILE"]) as fd: return cached_oauth_authorization_server()
return jsonify(json.load(fd))
@bp.route("/openid-configuration") @bp.route("/openid-configuration")
def openid_configuration(): def openid_configuration():
with open(current_app.config["OIDC_METADATA_FILE"]) as fd: return cached_openid_configuration()
return jsonify(json.load(fd))
@bp.route("/webfinger")
def webfinger():
return jsonify(
{
"links": [
{
"href": cached_openid_configuration()["issuer"],
"rel": "http://openid.net/specs/connect/1.0/issuer",
}
],
"subject": request.args["resource"],
}
)

View file

@ -215,3 +215,35 @@ expired tokens and authorization codes with:
.. code-block:: console .. code-block:: console
env CONFIG="$CANAILLE_CONF_DIR/config.toml" FLASK_APP=canaille "$CANAILLE_INSTALL_DIR/env/bin/canaille" clean env CONFIG="$CANAILLE_CONF_DIR/config.toml" FLASK_APP=canaille "$CANAILLE_INSTALL_DIR/env/bin/canaille" clean
Webfinger
=========
You may want to configure a `WebFinger`_ endpoint on your main website to allow the automatic discovery of your Canaille installation based on the account name of one of your users. For instance, suppose your domain is ``mydomain.tld`` and your Canaille domain is ``auth.mydomain.tld`` and there is an user ``john.doe``. A third-party application could require to authenticate the user and ask them for an user account. The user would give their account ``john.doe@mydomain.tld``, then the application would perform a WebFinger request at ``https://mydomain.tld/.well-known/webfinger`` and the response would contain the address of the authentication server ``https://auth.mydomain.tld``. With this information the third party application can redirect the user to the Canaille authentication page.
The difficulty here is that the WebFinger endpoint must be hosted at the top-level domain (i.e. ``mydomain.tld``) while the authentication server might be hosted on a sublevel (i.e. ``auth.mydomain.tld``). Canaille provides a WebFinger endpoint, but if it is not hosted at the top-level domain, a web redirection is required on the ``/.well-known/webfinger`` path.
Nginx
-----
.. code-block:: console
server {
listen 443;
server_name mydomain.tld;
rewrite ^/.well-known/webfinger https://auth.mydomain.tld/.well-known/webfinger permanent;
}
Apache
------
.. code-block:: console
<VirtualHost *:443>
ServerName mydomain.tld
RewriteEngine on
RewriteRule "^/.well-know/webfinger" "https://auth.mydomain.tld/.well-known/webfinger" [R,L]
</VirtualHost>
.. _WebFinger: https://www.rfc-editor.org/rfc/rfc7033.html

View file

@ -0,0 +1,32 @@
def test_issuer(testclient, user):
res = testclient.get(
"/.well-known/webfinger?resource=acct%3Auser%40mydomain.tld&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer"
)
assert res.json == {
"subject": "acct:user@mydomain.tld",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://auth.mydomain.tld",
}
],
}
def test_resource_unknown(testclient):
res = testclient.get(
"/.well-known/webfinger?resource=acct%3Ainvalid%40mydomain.tld&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer",
)
assert res.json == {
"subject": "acct:invalid@mydomain.tld",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://auth.mydomain.tld",
}
],
}
def test_bad_request(testclient, user):
testclient.get("/.well-known/webfinger", status=400)

View file

@ -1,8 +1,8 @@
def test_oauth_authorization_server(testclient): def test_oauth_authorization_server(testclient):
res = testclient.get("/.well-known/oauth-authorization-server", status=200).json res = testclient.get("/.well-known/oauth-authorization-server", status=200).json
assert "https://mydomain.tld" == res["issuer"] assert "https://auth.mydomain.tld" == res["issuer"]
def test_openid_configuration(testclient): def test_openid_configuration(testclient):
res = testclient.get("/.well-known/openid-configuration", status=200).json res = testclient.get("/.well-known/openid-configuration", status=200).json
assert "https://mydomain.tld" == res["issuer"] assert "https://auth.mydomain.tld" == res["issuer"]