metadata.py 10 KB

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