1""" 
    2oauthlib.oauth2.rfc6749.endpoint.metadata 
    3~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    4 
    5An implementation of the `OAuth 2.0 Authorization Server Metadata`. 
    6 
    7.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414 
    8""" 
    9import copy 
    10import json 
    11import logging 
    12 
    13from .. import grant_types, utils 
    14from .authorization import AuthorizationEndpoint 
    15from .base import BaseEndpoint, catch_errors_and_unavailability 
    16from .introspect import IntrospectEndpoint 
    17from .revocation import RevocationEndpoint 
    18from .token import TokenEndpoint 
    19 
    20log = logging.getLogger(__name__) 
    21 
    22 
    23class MetadataEndpoint(BaseEndpoint): 
    24 
    25    """OAuth2.0 Authorization Server Metadata endpoint. 
    26 
    27   This specification generalizes the metadata format defined by 
    28   `OpenID Connect Discovery 1.0` in a way that is compatible 
    29   with OpenID Connect Discovery while being applicable to a wider set 
    30   of OAuth 2.0 use cases.  This is intentionally parallel to the way 
    31   that OAuth 2.0 Dynamic Client Registration Protocol [`RFC7591`_] 
    32   generalized the dynamic client registration mechanisms defined by 
    33   OpenID Connect Dynamic Client Registration 1.0 
    34   in a way that is compatible with it. 
    35 
    36   .. _`OpenID Connect Discovery 1.0`: https://openid.net/specs/openid-connect-discovery-1_0.html 
    37   .. _`RFC7591`: https://tools.ietf.org/html/rfc7591 
    38   """ 
    39 
    40    def __init__(self, endpoints, claims={}, raise_errors=True): 
    41        assert isinstance(claims, dict)  # noqa: S101 
    42        for endpoint in endpoints: 
    43            assert isinstance(endpoint, BaseEndpoint)  # noqa: S101 
    44 
    45        BaseEndpoint.__init__(self) 
    46        self.raise_errors = raise_errors 
    47        self.endpoints = endpoints 
    48        self.initial_claims = claims 
    49        self.claims = self.validate_metadata_server() 
    50 
    51    @catch_errors_and_unavailability 
    52    def create_metadata_response(self, uri, http_method='GET', body=None, 
    53                                 headers=None): 
    54        """Create metadata response 
    55        """ 
    56        headers = { 
    57            'Content-Type': 'application/json', 
    58            'Access-Control-Allow-Origin': '*', 
    59        } 
    60        return headers, json.dumps(self.claims), 200 
    61 
    62    def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False): 
    63        if not self.raise_errors: 
    64            return 
    65 
    66        if key not in array: 
    67            if is_required: 
    68                raise ValueError("key {} is a mandatory metadata.".format(key)) 
    69 
    70        elif is_issuer: 
    71            if not utils.is_secure_transport(array[key]): 
    72                raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key])) 
    73            if "?" in array[key] or "&" in array[key] or "#" in array[key]: 
    74                raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key])) 
    75 
    76        elif is_url: 
    77            if not array[key].startswith("http"): 
    78                raise ValueError("key {}: {} must be an URL".format(key, array[key])) 
    79 
    80        elif is_list: 
    81            if not isinstance(array[key], list): 
    82                raise ValueError("key {}: {} must be an Array".format(key, array[key])) 
    83            for elem in array[key]: 
    84                if not isinstance(elem, str): 
    85                    raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem)) 
    86 
    87    def validate_metadata_token(self, claims, endpoint): 
    88        """ 
    89        If the token endpoint is used in the grant type, the value of this 
    90        parameter MUST be the same as the value of the "grant_type" 
    91        parameter passed to the token endpoint defined in the grant type 
    92        definition. 
    93        """ 
    94        self._grant_types.extend(endpoint._grant_types.keys()) 
    95        claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"]) 
    96 
    97        self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True) 
    98        self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True) 
    99        self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True) 
    100 
    101    def validate_metadata_authorization(self, claims, endpoint): 
    102        claims.setdefault("response_types_supported", 
    103                          list(filter(lambda x: x != "none", endpoint._response_types.keys()))) 
    104        claims.setdefault("response_modes_supported", ["query", "fragment"]) 
    105 
    106        # The OAuth2.0 Implicit flow is defined as a "grant type" but it is not 
    107        # using the "token" endpoint, as such, we have to add it explicitly to 
    108        # the list of "grant_types_supported" when enabled. 
    109        if "token" in claims["response_types_supported"]: 
    110            self._grant_types.append("implicit") 
    111 
    112        self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) 
    113        self.validate_metadata(claims, "response_modes_supported", is_list=True) 
    114        if "code" in claims["response_types_supported"]: 
    115            code_grant = endpoint._response_types["code"] 
    116            if not isinstance(code_grant, grant_types.AuthorizationCodeGrant) and hasattr(code_grant, "default_grant"): 
    117                code_grant = code_grant.default_grant 
    118 
    119            claims.setdefault("code_challenge_methods_supported", 
    120                              list(code_grant._code_challenge_methods.keys())) 
    121            self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True) 
    122        self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True) 
    123 
    124    def validate_metadata_revocation(self, claims, endpoint): 
    125        claims.setdefault("revocation_endpoint_auth_methods_supported", 
    126                          ["client_secret_post", "client_secret_basic"]) 
    127 
    128        self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True) 
    129        self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True) 
    130        self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True) 
    131 
    132    def validate_metadata_introspection(self, claims, endpoint): 
    133        claims.setdefault("introspection_endpoint_auth_methods_supported", 
    134                          ["client_secret_post", "client_secret_basic"]) 
    135 
    136        self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True) 
    137        self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True) 
    138        self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True) 
    139 
    140    def validate_metadata_server(self): 
    141        """ 
    142        Authorization servers can have metadata describing their 
    143        configuration.  The following authorization server metadata values 
    144        are used by this specification. More details can be found in 
    145        `RFC8414 section 2`_ : 
    146 
    147       issuer 
    148          REQUIRED 
    149 
    150       authorization_endpoint 
    151          URL of the authorization server's authorization endpoint 
    152          [`RFC6749#Authorization`_].  This is REQUIRED unless no grant types are supported 
    153          that use the authorization endpoint. 
    154 
    155       token_endpoint 
    156          URL of the authorization server's token endpoint [`RFC6749#Token`_].  This 
    157          is REQUIRED unless only the implicit grant type is supported. 
    158 
    159       scopes_supported 
    160          RECOMMENDED. 
    161 
    162       response_types_supported 
    163          REQUIRED. 
    164 
    165       Other OPTIONAL fields: 
    166          jwks_uri, 
    167          registration_endpoint, 
    168          response_modes_supported 
    169 
    170       grant_types_supported 
    171          OPTIONAL.  JSON array containing a list of the OAuth 2.0 grant 
    172          type values that this authorization server supports.  The array 
    173          values used are the same as those used with the "grant_types" 
    174          parameter defined by "OAuth 2.0 Dynamic Client Registration 
    175          Protocol" [`RFC7591`_].  If omitted, the default value is 
    176          "["authorization_code", "implicit"]". 
    177 
    178       token_endpoint_auth_methods_supported 
    179 
    180       token_endpoint_auth_signing_alg_values_supported 
    181 
    182       service_documentation 
    183 
    184       ui_locales_supported 
    185 
    186       op_policy_uri 
    187 
    188       op_tos_uri 
    189 
    190       revocation_endpoint 
    191 
    192       revocation_endpoint_auth_methods_supported 
    193 
    194       revocation_endpoint_auth_signing_alg_values_supported 
    195 
    196       introspection_endpoint 
    197 
    198       introspection_endpoint_auth_methods_supported 
    199 
    200       introspection_endpoint_auth_signing_alg_values_supported 
    201 
    202       code_challenge_methods_supported 
    203 
    204       Additional authorization server metadata parameters MAY also be used. 
    205       Some are defined by other specifications, such as OpenID Connect 
    206       Discovery 1.0 [`OpenID.Discovery`_]. 
    207 
    208        .. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2 
    209        .. _`RFC6749#Authorization`: https://tools.ietf.org/html/rfc6749#section-3.1 
    210        .. _`RFC6749#Token`: https://tools.ietf.org/html/rfc6749#section-3.2 
    211        .. _`RFC7591`: https://tools.ietf.org/html/rfc7591 
    212        .. _`OpenID.Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html 
    213        """ 
    214        claims = copy.deepcopy(self.initial_claims) 
    215        self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True) 
    216        self.validate_metadata(claims, "jwks_uri", is_url=True) 
    217        self.validate_metadata(claims, "scopes_supported", is_list=True) 
    218        self.validate_metadata(claims, "service_documentation", is_url=True) 
    219        self.validate_metadata(claims, "ui_locales_supported", is_list=True) 
    220        self.validate_metadata(claims, "op_policy_uri", is_url=True) 
    221        self.validate_metadata(claims, "op_tos_uri", is_url=True) 
    222 
    223        self._grant_types = [] 
    224        for endpoint in self.endpoints: 
    225            if isinstance(endpoint, TokenEndpoint): 
    226                self.validate_metadata_token(claims, endpoint) 
    227            if isinstance(endpoint, AuthorizationEndpoint): 
    228                self.validate_metadata_authorization(claims, endpoint) 
    229            if isinstance(endpoint, RevocationEndpoint): 
    230                self.validate_metadata_revocation(claims, endpoint) 
    231            if isinstance(endpoint, IntrospectEndpoint): 
    232                self.validate_metadata_introspection(claims, endpoint) 
    233 
    234        # "grant_types_supported" is a combination of all OAuth2 grant types 
    235        # allowed in the current provider implementation. 
    236        claims.setdefault("grant_types_supported", self._grant_types) 
    237        self.validate_metadata(claims, "grant_types_supported", is_list=True) 
    238        return claims