updates
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from .authorization_server import JWTAuthenticationRequest
|
||||
from .discovery import AuthorizationServerMetadata
|
||||
from .registration import ClientMetadataClaims
|
||||
|
||||
__all__ = [
|
||||
"AuthorizationServerMetadata",
|
||||
"JWTAuthenticationRequest",
|
||||
"ClientMetadataClaims",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,255 @@
|
||||
from authlib.jose import jwt
|
||||
from authlib.jose.errors import JoseError
|
||||
|
||||
from ..rfc6749 import AuthorizationServer
|
||||
from ..rfc6749 import ClientMixin
|
||||
from ..rfc6749 import InvalidRequestError
|
||||
from ..rfc6749.authenticate_client import _validate_client
|
||||
from ..rfc6749.requests import BasicOAuth2Payload
|
||||
from ..rfc6749.requests import OAuth2Request
|
||||
from .errors import InvalidRequestObjectError
|
||||
from .errors import InvalidRequestUriError
|
||||
from .errors import RequestNotSupportedError
|
||||
from .errors import RequestUriNotSupportedError
|
||||
|
||||
|
||||
class JWTAuthenticationRequest:
|
||||
"""Authorization server extension implementing the support
|
||||
for JWT secured authentication request, as defined in :rfc:`RFC9101 <9101>`.
|
||||
|
||||
:param support_request: Whether to enable support for the ``request`` parameter.
|
||||
:param support_request_uri: Whether to enable support for the ``request_uri`` parameter.
|
||||
|
||||
This extension is intended to be inherited and registered into the authorization server::
|
||||
|
||||
class JWTAuthenticationRequest(rfc9101.JWTAuthenticationRequest):
|
||||
def resolve_client_public_key(self, client: ClientMixin):
|
||||
return get_jwks_for_client(client)
|
||||
|
||||
def get_request_object(self, request_uri: str):
|
||||
try:
|
||||
return requests.get(request_uri).text
|
||||
except requests.Exception:
|
||||
return None
|
||||
|
||||
def get_server_metadata(self):
|
||||
return {
|
||||
"issuer": ...,
|
||||
"authorization_endpoint": ...,
|
||||
"require_signed_request_object": ...,
|
||||
}
|
||||
|
||||
def get_client_require_signed_request_object(self, client: ClientMixin):
|
||||
return client.require_signed_request_object
|
||||
|
||||
|
||||
authorization_server.register_extension(JWTAuthenticationRequest())
|
||||
"""
|
||||
|
||||
def __init__(self, support_request: bool = True, support_request_uri: bool = True):
|
||||
self.support_request = support_request
|
||||
self.support_request_uri = support_request_uri
|
||||
|
||||
def __call__(self, authorization_server: AuthorizationServer):
|
||||
authorization_server.register_hook(
|
||||
"before_get_authorization_grant", self.parse_authorization_request
|
||||
)
|
||||
|
||||
def parse_authorization_request(
|
||||
self, authorization_server: AuthorizationServer, request: OAuth2Request
|
||||
):
|
||||
client = _validate_client(
|
||||
authorization_server.query_client, request.payload.client_id
|
||||
)
|
||||
if not self._shoud_proceed_with_request_object(
|
||||
authorization_server, request, client
|
||||
):
|
||||
return
|
||||
|
||||
raw_request_object = self._get_raw_request_object(authorization_server, request)
|
||||
request_object = self._decode_request_object(
|
||||
request, client, raw_request_object
|
||||
)
|
||||
payload = BasicOAuth2Payload(request_object)
|
||||
request.payload = payload
|
||||
|
||||
def _shoud_proceed_with_request_object(
|
||||
self,
|
||||
authorization_server: AuthorizationServer,
|
||||
request: OAuth2Request,
|
||||
client: ClientMixin,
|
||||
) -> bool:
|
||||
if "request" in request.payload.data and "request_uri" in request.payload.data:
|
||||
raise InvalidRequestError(
|
||||
"The 'request' and 'request_uri' parameters are mutually exclusive.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
if "request" in request.payload.data:
|
||||
if not self.support_request:
|
||||
raise RequestNotSupportedError(state=request.payload.state)
|
||||
return True
|
||||
|
||||
if "request_uri" in request.payload.data:
|
||||
if not self.support_request_uri:
|
||||
raise RequestUriNotSupportedError(state=request.payload.state)
|
||||
return True
|
||||
|
||||
# When the value of it [require_signed_request_object] as client metadata is true,
|
||||
# then the server MUST reject the authorization request
|
||||
# from the client that does not conform to this specification.
|
||||
if self.get_client_require_signed_request_object(client):
|
||||
raise InvalidRequestError(
|
||||
"Authorization requests for this client must use signed request objects.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
# When the value of it [require_signed_request_object] as server metadata is true,
|
||||
# then the server MUST reject the authorization request
|
||||
# from any client that does not conform to this specification.
|
||||
metadata = self.get_server_metadata()
|
||||
if metadata and metadata.get("require_signed_request_object", False):
|
||||
raise InvalidRequestError(
|
||||
"Authorization requests for this server must use signed request objects.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def _get_raw_request_object(
|
||||
self, authorization_server: AuthorizationServer, request: OAuth2Request
|
||||
) -> str:
|
||||
if "request_uri" in request.payload.data:
|
||||
raw_request_object = self.get_request_object(
|
||||
request.payload.data["request_uri"]
|
||||
)
|
||||
if not raw_request_object:
|
||||
raise InvalidRequestUriError(state=request.payload.state)
|
||||
|
||||
else:
|
||||
raw_request_object = request.payload.data["request"]
|
||||
|
||||
return raw_request_object
|
||||
|
||||
def _decode_request_object(
|
||||
self, request, client: ClientMixin, raw_request_object: str
|
||||
):
|
||||
jwks = self.resolve_client_public_key(client)
|
||||
|
||||
try:
|
||||
request_object = jwt.decode(raw_request_object, jwks)
|
||||
request_object.validate()
|
||||
|
||||
except JoseError as error:
|
||||
raise InvalidRequestObjectError(
|
||||
description=error.description or InvalidRequestObjectError.description,
|
||||
state=request.payload.state,
|
||||
) from error
|
||||
|
||||
# It MUST also reject the request if the Request Object uses an
|
||||
# alg value of none when this server metadata value is true.
|
||||
# If omitted, the default value is false.
|
||||
if (
|
||||
self.get_client_require_signed_request_object(client)
|
||||
and request_object.header["alg"] == "none"
|
||||
):
|
||||
raise InvalidRequestError(
|
||||
"Authorization requests for this client must use signed request objects.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
# It MUST also reject the request if the Request Object uses an
|
||||
# alg value of none. If omitted, the default value is false.
|
||||
metadata = self.get_server_metadata()
|
||||
if (
|
||||
metadata
|
||||
and metadata.get("require_signed_request_object", False)
|
||||
and request_object.header["alg"] == "none"
|
||||
):
|
||||
raise InvalidRequestError(
|
||||
"Authorization requests for this server must use signed request objects.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
# The client ID values in the client_id request parameter and in
|
||||
# the Request Object client_id claim MUST be identical.
|
||||
if request_object["client_id"] != request.payload.client_id:
|
||||
raise InvalidRequestError(
|
||||
"The 'client_id' claim from the request parameters "
|
||||
"and the request object claims don't match.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
# The Request Object MAY be sent by value, as described in Section 5.1,
|
||||
# or by reference, as described in Section 5.2. request and
|
||||
# request_uri parameters MUST NOT be included in Request Objects.
|
||||
if "request" in request_object or "request_uri" in request_object:
|
||||
raise InvalidRequestError(
|
||||
"The 'request' and 'request_uri' parameters must not be included in the request object.",
|
||||
state=request.payload.state,
|
||||
)
|
||||
|
||||
return request_object
|
||||
|
||||
def get_request_object(self, request_uri: str):
|
||||
"""Download the request object at ``request_uri``.
|
||||
|
||||
This method must be implemented if the ``request_uri`` parameter is supported::
|
||||
|
||||
class JWTAuthenticationRequest(rfc9101.JWTAuthenticationRequest):
|
||||
def get_request_object(self, request_uri: str):
|
||||
try:
|
||||
return requests.get(request_uri).text
|
||||
except requests.Exception:
|
||||
return None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def resolve_client_public_keys(self, client: ClientMixin):
|
||||
"""Resolve the client public key for verifying the JWT signature.
|
||||
A client may have many public keys, in this case, we can retrieve it
|
||||
via ``kid`` value in headers. Developers MUST implement this method::
|
||||
|
||||
class JWTAuthenticationRequest(rfc9101.JWTAuthenticationRequest):
|
||||
def resolve_client_public_key(self, client):
|
||||
if client.jwks_uri:
|
||||
return requests.get(client.jwks_uri).json
|
||||
|
||||
return client.jwks
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_server_metadata(self) -> dict:
|
||||
"""Return server metadata which includes supported grant types,
|
||||
response types and etc.
|
||||
|
||||
When the ``require_signed_request_object`` claim is :data:`True`,
|
||||
all clients require that authorization requests
|
||||
use request objects, and an error will be returned when the authorization
|
||||
request payload is passed in the request body or query string::
|
||||
|
||||
class JWTAuthenticationRequest(rfc9101.JWTAuthenticationRequest):
|
||||
def get_server_metadata(self):
|
||||
return {
|
||||
"issuer": ...,
|
||||
"authorization_endpoint": ...,
|
||||
"require_signed_request_object": ...,
|
||||
}
|
||||
|
||||
"""
|
||||
return {} # pragma: no cover
|
||||
|
||||
def get_client_require_signed_request_object(self, client: ClientMixin) -> bool:
|
||||
"""Return the 'require_signed_request_object' client metadata.
|
||||
|
||||
When :data:`True`, the client requires that authorization requests
|
||||
use request objects, and an error will be returned when the authorization
|
||||
request payload is passed in the request body or query string::
|
||||
|
||||
class JWTAuthenticationRequest(rfc9101.JWTAuthenticationRequest):
|
||||
def get_client_require_signed_request_object(self, client):
|
||||
return client.require_signed_request_object
|
||||
|
||||
If not implemented, the value is considered as :data:`False`.
|
||||
"""
|
||||
return False # pragma: no cover
|
||||
@@ -0,0 +1,9 @@
|
||||
from authlib.oidc.discovery.models import _validate_boolean_value
|
||||
|
||||
|
||||
class AuthorizationServerMetadata(dict):
|
||||
REGISTRY_KEYS = ["require_signed_request_object"]
|
||||
|
||||
def validate_require_signed_request_object(self):
|
||||
"""Indicates where authorization request needs to be protected as Request Object and provided through either request or request_uri parameter."""
|
||||
_validate_boolean_value(self, "require_signed_request_object")
|
||||
@@ -0,0 +1,34 @@
|
||||
from ..base import OAuth2Error
|
||||
|
||||
__all__ = [
|
||||
"InvalidRequestUriError",
|
||||
"InvalidRequestObjectError",
|
||||
"RequestNotSupportedError",
|
||||
"RequestUriNotSupportedError",
|
||||
]
|
||||
|
||||
|
||||
class InvalidRequestUriError(OAuth2Error):
|
||||
error = "invalid_request_uri"
|
||||
description = "The request_uri in the authorization request returns an error or contains invalid data."
|
||||
status_code = 400
|
||||
|
||||
|
||||
class InvalidRequestObjectError(OAuth2Error):
|
||||
error = "invalid_request_object"
|
||||
description = "The request parameter contains an invalid Request Object."
|
||||
status_code = 400
|
||||
|
||||
|
||||
class RequestNotSupportedError(OAuth2Error):
|
||||
error = "request_not_supported"
|
||||
description = (
|
||||
"The authorization server does not support the use of the request parameter."
|
||||
)
|
||||
status_code = 400
|
||||
|
||||
|
||||
class RequestUriNotSupportedError(OAuth2Error):
|
||||
error = "request_uri_not_supported"
|
||||
description = "The authorization server does not support the use of the request_uri parameter."
|
||||
status_code = 400
|
||||
@@ -0,0 +1,44 @@
|
||||
from authlib.jose import BaseClaims
|
||||
from authlib.jose.errors import InvalidClaimError
|
||||
|
||||
|
||||
class ClientMetadataClaims(BaseClaims):
|
||||
"""Additional client metadata can be used with :ref:`specs/rfc7591` and :ref:`specs/rfc7592` endpoints.
|
||||
|
||||
This can be used with::
|
||||
|
||||
server.register_endpoint(
|
||||
ClientRegistrationEndpoint(
|
||||
claims_classes=[
|
||||
rfc7591.ClientMetadataClaims,
|
||||
rfc9101.ClientMetadataClaims,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
server.register_endpoint(
|
||||
ClientRegistrationEndpoint(
|
||||
claims_classes=[
|
||||
rfc7591.ClientMetadataClaims,
|
||||
rfc9101.ClientMetadataClaims,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
REGISTERED_CLAIMS = [
|
||||
"require_signed_request_object",
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
self._validate_essential_claims()
|
||||
self.validate_require_signed_request_object()
|
||||
|
||||
def validate_require_signed_request_object(self):
|
||||
self.setdefault("require_signed_request_object", False)
|
||||
|
||||
if not isinstance(self["require_signed_request_object"], bool):
|
||||
raise InvalidClaimError("require_signed_request_object")
|
||||
|
||||
self._validate_claim_value("require_signed_request_object")
|
||||
Reference in New Issue
Block a user