forked from Github-Mirrors/canaille
feat: OIDC prompt=create implementation
This commit is contained in:
parent
577bca360e
commit
c847ef9284
7 changed files with 167 additions and 2 deletions
|
@ -3,6 +3,11 @@ 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>`_.
|
||||
|
||||
Added
|
||||
*****
|
||||
|
||||
- OIDC `prompt=create` support. :issue:`185` :pr:`164`
|
||||
|
||||
Fixed
|
||||
*****
|
||||
|
||||
|
@ -13,6 +18,7 @@ Fixed
|
|||
|
||||
Added
|
||||
*****
|
||||
|
||||
- ``THEME`` can be a relative path
|
||||
|
||||
[0.0.39] - 2023-12-15
|
||||
|
|
|
@ -34,6 +34,7 @@ from .oauth import IntrospectionEndpoint
|
|||
from .oauth import require_oauth
|
||||
from .oauth import RevocationEndpoint
|
||||
from .utils import SCOPE_DETAILS
|
||||
from .well_known import openid_configuration
|
||||
|
||||
|
||||
bp = Blueprint("endpoints", __name__, url_prefix="/oauth")
|
||||
|
@ -54,6 +55,23 @@ def authorize():
|
|||
if not client:
|
||||
abort(400, "Invalid client.")
|
||||
|
||||
# https://openid.net/specs/openid-connect-prompt-create-1_0.html#name-authorization-request
|
||||
# If the OpenID Provider receives a prompt value that it does
|
||||
# not support (not declared in the prompt_values_supported
|
||||
# metadata field) the OP SHOULD respond with an HTTP 400 (Bad
|
||||
# Request) status code and an error value of invalid_request.
|
||||
# It is RECOMMENDED that the OP return an error_description
|
||||
# value identifying the invalid parameter value.
|
||||
if (
|
||||
request.args.get("prompt")
|
||||
and request.args["prompt"]
|
||||
not in openid_configuration()["prompt_values_supported"]
|
||||
):
|
||||
return {
|
||||
"error": "invalid_request",
|
||||
"error_description": f"prompt '{request.args['prompt'] }' value is not supported",
|
||||
}, 400
|
||||
|
||||
user = current_user()
|
||||
requested_scopes = request.args.get("scope", "").split(" ")
|
||||
allowed_scopes = client.get_allowed_scope(requested_scopes).split(" ")
|
||||
|
@ -65,6 +83,10 @@ def authorize():
|
|||
return jsonify({"error": "login_required"})
|
||||
|
||||
session["redirect-after-login"] = request.url
|
||||
|
||||
if request.args.get("prompt") == "create":
|
||||
return redirect(url_for("core.account.join"))
|
||||
|
||||
return redirect(url_for("core.auth.login"))
|
||||
|
||||
if not user.can_use_oidc:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
|
@ -76,7 +77,8 @@ def openid_configuration():
|
|||
],
|
||||
"subject_types_supported": ["pairwise", "public"],
|
||||
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
|
||||
"prompt_values_supported": ["none"],
|
||||
"prompt_values_supported": ["none"]
|
||||
+ (["create"] if current_app.config.get("ENABLE_REGISTRATION") else []),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ OpenID Connect
|
|||
- ❌ `OpenID Connect Back Channel Logout <https://openid.net/specs/openid-connect-backchannel-1_0.html>`_
|
||||
- ❌ `OpenID Connect Back Channel Authentication Flow <https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html>`_
|
||||
- ❌ `OpenID Connect Core Error Code unmet_authentication_requirements <https://openid.net/specs/openid-connect-unmet-authentication-requirements-1_0.html>`_
|
||||
- ❌ `Initiating User Registration via OpenID Connect 1.0 <https://openid.net/specs/openid-connect-prompt-create-1_0.html>`_
|
||||
- ✅ `Initiating User Registration via OpenID Connect 1.0 <https://openid.net/specs/openid-connect-prompt-create-1_0.html>`_
|
||||
|
||||
Comparison with other providers
|
||||
===============================
|
||||
|
|
|
@ -71,6 +71,10 @@ def test_registration_with_email_validation(testclient, backend, smtpd):
|
|||
res.form["family_name"] = "newuser"
|
||||
res = res.form.submit()
|
||||
|
||||
assert res.flashes == [
|
||||
("success", "Your account has been created successfully."),
|
||||
]
|
||||
|
||||
user = models.User.get()
|
||||
assert user
|
||||
user.delete()
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
Tests the behavior of Canaille depending on the OIDC 'prompt' parameter.
|
||||
https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
||||
"""
|
||||
import datetime
|
||||
import uuid
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from canaille.app import models
|
||||
from canaille.core.account import RegistrationPayload
|
||||
from flask import url_for
|
||||
|
||||
|
||||
def test_prompt_none(testclient, logged_user, client):
|
||||
|
@ -98,3 +101,125 @@ def test_prompt_no_consent(testclient, logged_user, client):
|
|||
status=200,
|
||||
)
|
||||
assert "consent_required" == res.json.get("error")
|
||||
|
||||
|
||||
def test_prompt_create_logged(testclient, logged_user, client):
|
||||
"""
|
||||
If prompt=create and user is already logged in,
|
||||
then go straight to the consent page.
|
||||
"""
|
||||
testclient.app.config["ENABLE_REGISTRATION"] = True
|
||||
|
||||
consent = models.Consent(
|
||||
consent_id=str(uuid.uuid4()),
|
||||
client=client,
|
||||
subject=logged_user,
|
||||
scope=["openid", "profile"],
|
||||
)
|
||||
consent.save()
|
||||
|
||||
res = testclient.get(
|
||||
"/oauth/authorize",
|
||||
params=dict(
|
||||
response_type="code",
|
||||
client_id=client.client_id,
|
||||
scope="openid profile",
|
||||
nonce="somenonce",
|
||||
prompt="create",
|
||||
),
|
||||
status=302,
|
||||
)
|
||||
assert res.location.startswith(client.redirect_uris[0])
|
||||
|
||||
consent.delete()
|
||||
|
||||
|
||||
def test_prompt_create_registration_disabled(testclient, trusted_client, smtpd):
|
||||
"""
|
||||
If prompt=create but Canaille registration is disabled,
|
||||
an error response should be returned.
|
||||
|
||||
If the OpenID Provider receives a prompt value that it does
|
||||
not support (not declared in the prompt_values_supported
|
||||
metadata field) the OP SHOULD respond with an HTTP 400 (Bad
|
||||
Request) status code and an error value of invalid_request.
|
||||
It is RECOMMENDED that the OP return an error_description
|
||||
value identifying the invalid parameter value.
|
||||
"""
|
||||
res = testclient.get(
|
||||
"/oauth/authorize",
|
||||
params=dict(
|
||||
response_type="code",
|
||||
client_id=trusted_client.client_id,
|
||||
scope="openid profile",
|
||||
nonce="somenonce",
|
||||
prompt="create",
|
||||
),
|
||||
status=400,
|
||||
)
|
||||
assert res.json == {
|
||||
"error": "invalid_request",
|
||||
"error_description": "prompt 'create' value is not supported",
|
||||
}
|
||||
|
||||
|
||||
def test_prompt_create_not_logged(testclient, trusted_client, smtpd):
|
||||
"""
|
||||
If prompt=create and user is not logged in,
|
||||
then display the registration form.
|
||||
Check that the user is correctly redirected to
|
||||
the client page after the registration process.
|
||||
"""
|
||||
testclient.app.config["ENABLE_REGISTRATION"] = True
|
||||
|
||||
res = testclient.get(
|
||||
"/oauth/authorize",
|
||||
params=dict(
|
||||
response_type="code",
|
||||
client_id=trusted_client.client_id,
|
||||
scope="openid profile",
|
||||
nonce="somenonce",
|
||||
prompt="create",
|
||||
),
|
||||
)
|
||||
|
||||
# Display the registration form
|
||||
res = res.follow()
|
||||
res.form["email"] = "foo@bar.com"
|
||||
res = res.form.submit()
|
||||
|
||||
# Checks the registration mail is sent
|
||||
assert len(smtpd.messages) == 1
|
||||
|
||||
# Simulate a click on the validation link in the mail
|
||||
payload = RegistrationPayload(
|
||||
creation_date_isoformat=datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).isoformat(),
|
||||
user_name="",
|
||||
user_name_editable=True,
|
||||
email="foo@bar.com",
|
||||
groups=[],
|
||||
)
|
||||
registration_url = url_for(
|
||||
"core.account.registration",
|
||||
data=payload.b64(),
|
||||
hash=payload.build_hash(),
|
||||
_external=True,
|
||||
)
|
||||
|
||||
# Fill the user creation form
|
||||
res = testclient.get(registration_url)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "password"
|
||||
res.form["password2"] = "password"
|
||||
res.form["family_name"] = "newuser"
|
||||
res = res.form.submit()
|
||||
|
||||
assert res.flashes == [
|
||||
("success", "Your account has been created successfully."),
|
||||
]
|
||||
|
||||
# Return to the client
|
||||
res = res.follow()
|
||||
assert res.location.startswith(trusted_client.redirect_uris[0])
|
||||
|
|
|
@ -100,3 +100,9 @@ def test_openid_configuration(testclient):
|
|||
"userinfo_endpoint": "http://canaille.test/oauth/userinfo",
|
||||
"prompt_values_supported": ["none"],
|
||||
}
|
||||
|
||||
|
||||
def test_openid_configuration_prompt_value_create(testclient):
|
||||
testclient.app.config["ENABLE_REGISTRATION"] = True
|
||||
res = testclient.get("/.well-known/openid-configuration", status=200).json
|
||||
assert "create" in res["prompt_values_supported"]
|
||||
|
|
Loading…
Reference in a new issue