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/>`_,
|
||||
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
|
||||
=====================
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
{
|
||||
"issuer":
|
||||
"https://mydomain.tld",
|
||||
"https://auth.mydomain.tld",
|
||||
"authorization_endpoint":
|
||||
"https://mydomain.tld/oauth/authorize",
|
||||
"https://auth.mydomain.tld/oauth/authorize",
|
||||
"token_endpoint":
|
||||
"https://mydomain.tld/oauth/token",
|
||||
"https://auth.mydomain.tld/oauth/token",
|
||||
"token_endpoint_auth_methods_supported":
|
||||
["client_secret_basic", "private_key_jwt",
|
||||
"client_secret_post", "none"],
|
||||
"token_endpoint_auth_signing_alg_values_supported":
|
||||
["RS256", "ES256"],
|
||||
"userinfo_endpoint":
|
||||
"https://mydomain.tld/oauth/userinfo",
|
||||
"https://auth.mydomain.tld/oauth/userinfo",
|
||||
"introspection_endpoint":
|
||||
"https://mydomain.tld/oauth/introspect",
|
||||
"https://auth.mydomain.tld/oauth/introspect",
|
||||
"jwks_uri":
|
||||
"https://mydomain.tld/oauth/jwks.json",
|
||||
"https://auth.mydomain.tld/oauth/jwks.json",
|
||||
"registration_endpoint":
|
||||
"https://mydomain.tld/oauth/register",
|
||||
"https://auth.mydomain.tld/oauth/register",
|
||||
"scopes_supported":
|
||||
["openid", "profile", "email", "address",
|
||||
"phone", "groups"],
|
||||
|
@ -25,7 +25,7 @@
|
|||
["code", "token", "id_token", "code token",
|
||||
"code id_token", "token id_token"],
|
||||
"service_documentation":
|
||||
"https://mydomain.tld/documentation.html",
|
||||
"https://auth.mydomain.tld/documentation.html",
|
||||
"ui_locales_supported":
|
||||
["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"issuer":
|
||||
"https://mydomain.tld",
|
||||
"https://auth.mydomain.tld",
|
||||
"authorization_endpoint":
|
||||
"https://mydomain.tld/oauth/authorize",
|
||||
"https://auth.mydomain.tld/oauth/authorize",
|
||||
"token_endpoint":
|
||||
"https://mydomain.tld/oauth/token",
|
||||
"https://auth.mydomain.tld/oauth/token",
|
||||
"token_endpoint_auth_methods_supported":
|
||||
["client_secret_basic", "private_key_jwt",
|
||||
"client_secret_post", "none"],
|
||||
"token_endpoint_auth_signing_alg_values_supported":
|
||||
["RS256"],
|
||||
"userinfo_endpoint":
|
||||
"https://mydomain.tld/oauth/userinfo",
|
||||
"https://auth.mydomain.tld/oauth/userinfo",
|
||||
"check_session_iframe":
|
||||
"https://mydomain.tld/oauth/check_session",
|
||||
"https://auth.mydomain.tld/oauth/check_session",
|
||||
"end_session_endpoint":
|
||||
"https://mydomain.tld/oauth/end_session",
|
||||
"https://auth.mydomain.tld/oauth/end_session",
|
||||
"jwks_uri":
|
||||
"https://mydomain.tld/oauth/jwks.json",
|
||||
"https://auth.mydomain.tld/oauth/jwks.json",
|
||||
"registration_endpoint":
|
||||
"https://mydomain.tld/oauth/register",
|
||||
"https://auth.mydomain.tld/oauth/register",
|
||||
"introspection_endpoint":
|
||||
"https://mydomain.tld/oauth/introspect",
|
||||
"https://auth.mydomain.tld/oauth/introspect",
|
||||
"scopes_supported":
|
||||
["openid", "profile", "email", "address",
|
||||
"phone", "groups"],
|
||||
|
@ -60,7 +60,7 @@
|
|||
"claims_parameter_supported":
|
||||
true,
|
||||
"service_documentation":
|
||||
"https://mydomain.tld/oauth/service_documentation.html",
|
||||
"https://auth.mydomain.tld/oauth/service_documentation.html",
|
||||
"ui_locales_supported":
|
||||
["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
|
||||
}
|
||||
|
|
|
@ -2,19 +2,48 @@ import json
|
|||
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
|
||||
|
||||
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")
|
||||
def oauth_authorization_server():
|
||||
with open(current_app.config["OAUTH2_METADATA_FILE"]) as fd:
|
||||
return jsonify(json.load(fd))
|
||||
return cached_oauth_authorization_server()
|
||||
|
||||
|
||||
@bp.route("/openid-configuration")
|
||||
def openid_configuration():
|
||||
with open(current_app.config["OIDC_METADATA_FILE"]) as fd:
|
||||
return jsonify(json.load(fd))
|
||||
return cached_openid_configuration()
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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):
|
||||
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):
|
||||
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