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