forked from Github-Mirrors/canaille
Implemented a basic WebFinger endpoint.
This commit is contained in:
parent
9100b8fb13
commit
e45ad6e21c
7 changed files with 126 additions and 24 deletions
|
@ -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
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
32
tests/oidc/test_webfinger.py
Normal file
32
tests/oidc/test_webfinger.py
Normal 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)
|
|
@ -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"]
|
||||||
|
|
Loading…
Reference in a new issue