1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
|
from authlib.common.security import is_secure_transport
from authlib.common.urls import is_valid_url
from authlib.common.urls import urlparse
class AuthorizationServerMetadata(dict):
"""Define Authorization Server Metadata via `Section 2`_ in RFC8414_.
.. _RFC8414: https://tools.ietf.org/html/rfc8414
.. _`Section 2`: https://tools.ietf.org/html/rfc8414#section-2
"""
REGISTRY_KEYS = [
"issuer",
"authorization_endpoint",
"token_endpoint",
"jwks_uri",
"registration_endpoint",
"scopes_supported",
"response_types_supported",
"response_modes_supported",
"grant_types_supported",
"token_endpoint_auth_methods_supported",
"token_endpoint_auth_signing_alg_values_supported",
"service_documentation",
"ui_locales_supported",
"op_policy_uri",
"op_tos_uri",
"revocation_endpoint",
"revocation_endpoint_auth_methods_supported",
"revocation_endpoint_auth_signing_alg_values_supported",
"introspection_endpoint",
"introspection_endpoint_auth_methods_supported",
"introspection_endpoint_auth_signing_alg_values_supported",
"code_challenge_methods_supported",
]
def validate_issuer(self):
"""REQUIRED. The authorization server's issuer identifier, which is
a URL that uses the "https" scheme and has no query or fragment
components.
"""
issuer = self.get("issuer")
#: 1. REQUIRED
if not issuer:
raise ValueError('"issuer" is required')
parsed = urlparse.urlparse(issuer)
#: 2. uses the "https" scheme
if not is_secure_transport(issuer):
raise ValueError('"issuer" MUST use "https" scheme')
#: 3. has no query or fragment
if parsed.query or parsed.fragment:
raise ValueError('"issuer" has no query or fragment')
def validate_authorization_endpoint(self):
"""URL of the authorization server's authorization endpoint
[RFC6749]. This is REQUIRED unless no grant types are supported
that use the authorization endpoint.
"""
url = self.get("authorization_endpoint")
if url:
if not is_secure_transport(url):
raise ValueError('"authorization_endpoint" MUST use "https" scheme')
return
grant_types_supported = set(self.grant_types_supported)
authorization_grant_types = {"authorization_code", "implicit"}
if grant_types_supported & authorization_grant_types:
raise ValueError('"authorization_endpoint" is required')
def validate_token_endpoint(self):
"""URL of the authorization server's token endpoint [RFC6749]. This
is REQUIRED unless only the implicit grant type is supported.
"""
grant_types_supported = self.get("grant_types_supported")
if (
grant_types_supported
and len(grant_types_supported) == 1
and grant_types_supported[0] == "implicit"
):
return
url = self.get("token_endpoint")
if not url:
raise ValueError('"token_endpoint" is required')
if not is_secure_transport(url):
raise ValueError('"token_endpoint" MUST use "https" scheme')
def validate_jwks_uri(self):
"""OPTIONAL. URL of the authorization server's JWK Set [JWK]
document. The referenced document contains the signing key(s) the
client uses to validate signatures from the authorization server.
This URL MUST use the "https" scheme. The JWK Set MAY also
contain the server's encryption key or keys, which are used by
clients to encrypt requests to the server. When both signing and
encryption keys are made available, a "use" (public key use)
parameter value is REQUIRED for all keys in the referenced JWK Set
to indicate each key's intended usage.
"""
url = self.get("jwks_uri")
if url and not is_secure_transport(url):
raise ValueError('"jwks_uri" MUST use "https" scheme')
def validate_registration_endpoint(self):
"""OPTIONAL. URL of the authorization server's OAuth 2.0 Dynamic
Client Registration endpoint [RFC7591].
"""
url = self.get("registration_endpoint")
if url and not is_secure_transport(url):
raise ValueError('"registration_endpoint" MUST use "https" scheme')
def validate_scopes_supported(self):
"""RECOMMENDED. JSON array containing a list of the OAuth 2.0
[RFC6749] "scope" values that this authorization server supports.
Servers MAY choose not to advertise some supported scope values
even when this parameter is used.
"""
validate_array_value(self, "scopes_supported")
def validate_response_types_supported(self):
"""REQUIRED. JSON array containing a list of the OAuth 2.0
"response_type" values that this authorization server supports.
The array values used are the same as those used with the
"response_types" parameter defined by "OAuth 2.0 Dynamic Client
Registration Protocol" [RFC7591].
"""
response_types_supported = self.get("response_types_supported")
if not response_types_supported:
raise ValueError('"response_types_supported" is required')
if not isinstance(response_types_supported, list):
raise ValueError('"response_types_supported" MUST be JSON array')
def validate_response_modes_supported(self):
"""OPTIONAL. JSON array containing a list of the OAuth 2.0
"response_mode" values that this authorization server supports, as
specified in "OAuth 2.0 Multiple Response Type Encoding Practices"
[OAuth.Responses]. If omitted, the default is "["query",
"fragment"]". The response mode value "form_post" is also defined
in "OAuth 2.0 Form Post Response Mode" [OAuth.Post].
"""
validate_array_value(self, "response_modes_supported")
def validate_grant_types_supported(self):
"""OPTIONAL. JSON array containing a list of the OAuth 2.0 grant
type values that this authorization server supports. The array
values used are the same as those used with the "grant_types"
parameter defined by "OAuth 2.0 Dynamic Client Registration
Protocol" [RFC7591]. If omitted, the default value is
"["authorization_code", "implicit"]".
"""
validate_array_value(self, "grant_types_supported")
def validate_token_endpoint_auth_methods_supported(self):
"""OPTIONAL. JSON array containing a list of client authentication
methods supported by this token endpoint. Client authentication
method values are used in the "token_endpoint_auth_method"
parameter defined in Section 2 of [RFC7591]. If omitted, the
default is "client_secret_basic" -- the HTTP Basic Authentication
Scheme specified in Section 2.3.1 of OAuth 2.0 [RFC6749].
"""
validate_array_value(self, "token_endpoint_auth_methods_supported")
def validate_token_endpoint_auth_signing_alg_values_supported(self):
"""OPTIONAL. JSON array containing a list of the JWS signing
algorithms ("alg" values) supported by the token endpoint for the
signature on the JWT [JWT] used to authenticate the client at the
token endpoint for the "private_key_jwt" and "client_secret_jwt"
authentication methods. This metadata entry MUST be present if
either of these authentication methods are specified in the
"token_endpoint_auth_methods_supported" entry. No default
algorithms are implied if this entry is omitted. Servers SHOULD
support "RS256". The value "none" MUST NOT be used.
"""
_validate_alg_values(
self,
"token_endpoint_auth_signing_alg_values_supported",
self.token_endpoint_auth_methods_supported,
)
def validate_service_documentation(self):
"""OPTIONAL. URL of a page containing human-readable information
that developers might want or need to know when using the
authorization server. In particular, if the authorization server
does not support Dynamic Client Registration, then information on
how to register clients needs to be provided in this
documentation.
"""
value = self.get("service_documentation")
if value and not is_valid_url(value):
raise ValueError('"service_documentation" MUST be a URL')
def validate_ui_locales_supported(self):
"""OPTIONAL. Languages and scripts supported for the user interface,
represented as a JSON array of language tag values from BCP 47
[RFC5646]. If omitted, the set of supported languages and scripts
is unspecified.
"""
validate_array_value(self, "ui_locales_supported")
def validate_op_policy_uri(self):
"""OPTIONAL. URL that the authorization server provides to the
person registering the client to read about the authorization
server's requirements on how the client can use the data provided
by the authorization server. The registration process SHOULD
display this URL to the person registering the client if it is
given. As described in Section 5, despite the identifier
"op_policy_uri" appearing to be OpenID-specific, its usage in this
specification is actually referring to a general OAuth 2.0 feature
that is not specific to OpenID Connect.
"""
value = self.get("op_policy_uri")
if value and not is_valid_url(value):
raise ValueError('"op_policy_uri" MUST be a URL')
def validate_op_tos_uri(self):
"""OPTIONAL. URL that the authorization server provides to the
person registering the client to read about the authorization
server's terms of service. The registration process SHOULD
display this URL to the person registering the client if it is
given. As described in Section 5, despite the identifier
"op_tos_uri", appearing to be OpenID-specific, its usage in this
specification is actually referring to a general OAuth 2.0 feature
that is not specific to OpenID Connect.
"""
value = self.get("op_tos_uri")
if value and not is_valid_url(value):
raise ValueError('"op_tos_uri" MUST be a URL')
def validate_revocation_endpoint(self):
"""OPTIONAL. URL of the authorization server's OAuth 2.0 revocation
endpoint [RFC7009].
"""
url = self.get("revocation_endpoint")
if url and not is_secure_transport(url):
raise ValueError('"revocation_endpoint" MUST use "https" scheme')
def validate_revocation_endpoint_auth_methods_supported(self):
"""OPTIONAL. JSON array containing a list of client authentication
methods supported by this revocation endpoint. The valid client
authentication method values are those registered in the IANA
"OAuth Token Endpoint Authentication Methods" registry
[IANA.OAuth.Parameters]. If omitted, the default is
"client_secret_basic" -- the HTTP Basic Authentication Scheme
specified in Section 2.3.1 of OAuth 2.0 [RFC6749].
"""
validate_array_value(self, "revocation_endpoint_auth_methods_supported")
def validate_revocation_endpoint_auth_signing_alg_values_supported(self):
"""OPTIONAL. JSON array containing a list of the JWS signing
algorithms ("alg" values) supported by the revocation endpoint for
the signature on the JWT [JWT] used to authenticate the client at
the revocation endpoint for the "private_key_jwt" and
"client_secret_jwt" authentication methods. This metadata entry
MUST be present if either of these authentication methods are
specified in the "revocation_endpoint_auth_methods_supported"
entry. No default algorithms are implied if this entry is
omitted. The value "none" MUST NOT be used.
"""
_validate_alg_values(
self,
"revocation_endpoint_auth_signing_alg_values_supported",
self.revocation_endpoint_auth_methods_supported,
)
def validate_introspection_endpoint(self):
"""OPTIONAL. URL of the authorization server's OAuth 2.0
introspection endpoint [RFC7662].
"""
url = self.get("introspection_endpoint")
if url and not is_secure_transport(url):
raise ValueError('"introspection_endpoint" MUST use "https" scheme')
def validate_introspection_endpoint_auth_methods_supported(self):
"""OPTIONAL. JSON array containing a list of client authentication
methods supported by this introspection endpoint. The valid
client authentication method values are those registered in the
IANA "OAuth Token Endpoint Authentication Methods" registry
[IANA.OAuth.Parameters] or those registered in the IANA "OAuth
Access Token Types" registry [IANA.OAuth.Parameters]. (These
values are and will remain distinct, due to Section 7.2.) If
omitted, the set of supported authentication methods MUST be
determined by other means.
"""
validate_array_value(self, "introspection_endpoint_auth_methods_supported")
def validate_introspection_endpoint_auth_signing_alg_values_supported(self):
"""OPTIONAL. JSON array containing a list of the JWS signing
algorithms ("alg" values) supported by the introspection endpoint
for the signature on the JWT [JWT] used to authenticate the client
at the introspection endpoint for the "private_key_jwt" and
"client_secret_jwt" authentication methods. This metadata entry
MUST be present if either of these authentication methods are
specified in the "introspection_endpoint_auth_methods_supported"
entry. No default algorithms are implied if this entry is
omitted. The value "none" MUST NOT be used.
"""
_validate_alg_values(
self,
"introspection_endpoint_auth_signing_alg_values_supported",
self.introspection_endpoint_auth_methods_supported,
)
def validate_code_challenge_methods_supported(self):
"""OPTIONAL. JSON array containing a list of Proof Key for Code
Exchange (PKCE) [RFC7636] code challenge methods supported by this
authorization server. Code challenge method values are used in
the "code_challenge_method" parameter defined in Section 4.3 of
[RFC7636]. The valid code challenge method values are those
registered in the IANA "PKCE Code Challenge Methods" registry
[IANA.OAuth.Parameters]. If omitted, the authorization server
does not support PKCE.
"""
validate_array_value(self, "code_challenge_methods_supported")
@property
def response_modes_supported(self):
#: If omitted, the default is ["query", "fragment"]
return self.get("response_modes_supported", ["query", "fragment"])
@property
def grant_types_supported(self):
#: If omitted, the default value is ["authorization_code", "implicit"]
return self.get("grant_types_supported", ["authorization_code", "implicit"])
@property
def token_endpoint_auth_methods_supported(self):
#: If omitted, the default is "client_secret_basic"
return self.get(
"token_endpoint_auth_methods_supported", ["client_secret_basic"]
)
@property
def revocation_endpoint_auth_methods_supported(self):
#: If omitted, the default is "client_secret_basic"
return self.get(
"revocation_endpoint_auth_methods_supported", ["client_secret_basic"]
)
@property
def introspection_endpoint_auth_methods_supported(self):
#: If omitted, the set of supported authentication methods MUST be
#: determined by other means
#: here, we use "client_secret_basic"
return self.get(
"introspection_endpoint_auth_methods_supported", ["client_secret_basic"]
)
def validate(self):
"""Validate all server metadata value."""
for key in self.REGISTRY_KEYS:
object.__getattribute__(self, f"validate_{key}")()
def __getattr__(self, key):
try:
return object.__getattribute__(self, key)
except AttributeError as error:
if key in self.REGISTRY_KEYS:
return self.get(key)
raise error
def _validate_alg_values(data, key, auth_methods_supported):
value = data.get(key)
if value and not isinstance(value, list):
raise ValueError(f'"{key}" MUST be JSON array')
auth_methods = set(auth_methods_supported)
jwt_auth_methods = {"private_key_jwt", "client_secret_jwt"}
if auth_methods & jwt_auth_methods:
if not value:
raise ValueError(f'"{key}" is required')
if value and "none" in value:
raise ValueError(f'the value "none" MUST NOT be used in "{key}"')
def validate_array_value(metadata, key):
values = metadata.get(key)
if values is not None and not isinstance(values, list):
raise ValueError(f'"{key}" MUST be JSON array')
|