Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions authlib/oidc/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,15 @@ class RegistrationNotSupportedError(OAuth2Error):
"""The OP does not support use of the registration parameter."""

error = "registration_not_supported"


class RegistrationRequiredError(OAuth2Error):
"""The Authorization Server requires the End-User to register.
This is used to indicate the authorization request included the
`prompt=create` value and the server wants to redirect the user
to a registration page. Integrations can raise this with
``redirect_uri`` to perform the redirect.
"""

error = "registration_required"
3 changes: 3 additions & 0 deletions authlib/oidc/core/grants/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ def create_response_mode_response(redirect_uri, params, response_mode):
def _guess_prompt_value(end_user, prompts, redirect_uri, redirect_fragment):
# http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest

if "create" in prompts:
return "create"

if not end_user or "login" in prompts:
return "login"

Expand Down
18 changes: 18 additions & 0 deletions authlib/oidc/discovery/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class OpenIDProviderMetadata(AuthorizationServerMetadata):
"request_parameter_supported",
"request_uri_parameter_supported",
"require_request_uri_registration",
"prompt_values_supported",
# not defined by OpenID
# 'revocation_endpoint',
# 'revocation_endpoint_auth_methods_supported',
Expand Down Expand Up @@ -260,6 +261,23 @@ def validate_request_uri_parameter_supported(self):
"""
validate_boolean_value(self, "request_uri_parameter_supported")

def validate_prompt_values_supported(self):
"""OPTIONAL. JSON array containing the list of prompt values that
this OpenID Provider supports. Values defined by OpenID Connect Core
are: "none", "login", "consent", "select_account". This
specification defines the additional value "create".
"""
values = self.get("prompt_values_supported")
if not values:
return

if not isinstance(values, list):
raise ValueError('"prompt_values_supported" MUST be JSON array')

valid_values = {"none", "login", "consent", "select_account", "create"}
if not valid_values.issuperset(set(values)):
raise ValueError('"prompt_values_supported" contains invalid values')

def validate_require_request_uri_registration(self):
"""OPTIONAL. Boolean value specifying whether the OP requires any
request_uri values used to be pre-registered using the request_uris
Expand Down
17 changes: 17 additions & 0 deletions docs/oauth2/specs/oidc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ OpenID Claims
.. autoclass:: UserInfo
:members:

Prompt Create Extension
-----------------------

Authlib supports the
`Initiating User Registration via OpenID Connect 1.0 <https://openid.net/specs/openid-connect-prompt-create-1_0.html>`__
extension.

This extension adds a ``create`` value for the ``prompt`` authorization
request parameter, which can be used to initiate a user registration flow.

Authlib also supports advertising supported prompt values through OpenID
Provider discovery metadata via ``prompt_values_supported``.

When using OpenID Connect grants (for example ``OpenIDCode``),
``prompt=create`` is parsed and exposed on the grant as ``grant.prompt`` for
application-specific handling in your authorization flow.

Dynamic client registration
---------------------------

Expand Down
5 changes: 5 additions & 0 deletions tests/core/test_oidc/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ def test_validate_display_values_supported():
_call_contains_invalid_value("display_values_supported", ["invalid"])


def test_validate_prompt_values_supported():
_call_validate_array("prompt_values_supported", ["none", "login", "create"])
_call_contains_invalid_value("prompt_values_supported", ["invalid"])


def test_validate_claim_types_supported():
_call_validate_array("claim_types_supported", ["normal"])
_call_contains_invalid_value("claim_types_supported", ["invalid"])
Expand Down
42 changes: 42 additions & 0 deletions tests/django/test_oauth2/test_authorization_code_grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from authlib.common.urls import urlparse
from authlib.oauth2.rfc6749 import errors
from authlib.oauth2.rfc6749 import grants
from authlib.oidc.core.grants import OpenIDCode as _OpenIDCode

from .models import Client
from .models import CodeGrantMixin
Expand Down Expand Up @@ -56,6 +57,23 @@ def client(user):
client.delete()


@pytest.fixture
def oidc_server(server):
class OpenIDCode(_OpenIDCode):
pass

grant_cls, _ = server._authorization_grants[0]
server._authorization_grants = [(grant_cls, [OpenIDCode()])]
return server


@pytest.fixture
def openid_client(client):
client.scope = "openid"
client.save()
return client


def test_get_consent_grant_client(factory, server, client):
url = "/authorize?response_type=code"
request = factory.get(url)
Expand Down Expand Up @@ -119,6 +137,30 @@ def test_create_authorization_response(factory, server):
assert "code=" in resp["Location"]


def test_prompt_create_sets_grant_prompt(factory, oidc_server, openid_client):
data = {
"response_type": "code",
"client_id": "client-id",
"redirect_uri": "https://client.test",
"scope": "openid",
"prompt": "create",
}
request = factory.post("/authorize", data=data)
grant = oidc_server.get_consent_grant(request)

resp = oidc_server.create_authorization_response(request, grant=grant)
assert resp.status_code == 302
assert "error=access_denied" in resp["Location"]

grant_user = User.objects.get(username="foo")
resp = oidc_server.create_authorization_response(
request, grant=grant, grant_user=grant_user
)
assert resp.status_code == 302
assert "code=" in resp["Location"]
assert grant.prompt == "create"


def test_create_token_response_invalid(factory, server):
# case: no auth
request = factory.post("/oauth/token", data={"grant_type": "authorization_code"})
Expand Down
37 changes: 37 additions & 0 deletions tests/flask/test_oauth2/test_openid_code_grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ def test_prompt(test_client, server):
rv = test_client.get("/oauth/authorize?" + query)
assert rv.data == b"login"

query = url_encode(params + [("user_id", "1"), ("prompt", "create")])
rv = test_client.get("/oauth/authorize?" + query)
assert rv.data == b"create"


def test_prompt_none_not_logged(test_client, server):
register_oidc_code_grant(
Expand All @@ -248,6 +252,39 @@ def test_prompt_none_not_logged(test_client, server):
assert params["state"] == "bar"


def test_prompt_create_is_retained(test_client, server):
register_oidc_code_grant(server)
params = [
("response_type", "code"),
("client_id", "client-id"),
("state", "bar"),
("nonce", "abc"),
("scope", "openid profile"),
("redirect_uri", "https://client.test"),
("user_id", "1"),
("prompt", "create"),
]
query = url_encode(params)
rv = test_client.get("/oauth/authorize?" + query)
assert rv.data == b"create"


def test_prompt_multiple_values_prefers_login_without_user(test_client, server):
register_oidc_code_grant(server)
params = [
("response_type", "code"),
("client_id", "client-id"),
("state", "bar"),
("nonce", "abc"),
("scope", "openid profile"),
("redirect_uri", "https://client.test"),
("prompt", "login consent"),
]
query = url_encode(params)
rv = test_client.get("/oauth/authorize?" + query)
assert rv.data == b"login"


def test_client_metadata_custom_alg(test_client, server, client, db, app):
"""The client metadata 'id_token_signed_response_alg' must take
precedence over the server-wide ``get_jwt_config()`` ``alg``."""
Expand Down
Loading