summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Huot <JonathanHuot@users.noreply.github.com>2018-11-23 09:19:38 +0100
committerGitHub <noreply@github.com>2018-11-23 09:19:38 +0100
commit50dfc47d2dc1fdd9f3f66af1b38ea36c7edc17b1 (patch)
treee7def291e931dadcc6c2ea95be8f8345e08f257e
parent10acc015b7f9a5e166fa3a9afeed8c1b531fa026 (diff)
parentcb6db1cdec841e3404cff68757a20cb675727e6e (diff)
downloadoauthlib-613-oidc-dispatcher.tar.gz
Merge branch 'master' into 613-oidc-dispatcher613-oidc-dispatcher
-rw-r--r--docs/feature_matrix.rst1
-rw-r--r--oauthlib/oauth2/__init__.py1
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/__init__.py1
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/metadata.py193
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_metadata.py38
5 files changed, 234 insertions, 0 deletions
diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst
index 59f3f3a..672cc27 100644
--- a/docs/feature_matrix.rst
+++ b/docs/feature_matrix.rst
@@ -18,6 +18,7 @@ OAuth 2 client and provider support for
- Draft MAC tokens
- Token Revocation
- Token Introspection
+- OAuth Authorization Server Metadata
- OpenID Connect Authentication
with support for SAML2 and JWT tokens, dynamic client registration and more to
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py
index 303c6a1..3f43755 100644
--- a/oauthlib/oauth2/__init__.py
+++ b/oauthlib/oauth2/__init__.py
@@ -16,6 +16,7 @@ from .rfc6749.clients import BackendApplicationClient
from .rfc6749.clients import ServiceApplicationClient
from .rfc6749.endpoints import AuthorizationEndpoint
from .rfc6749.endpoints import IntrospectEndpoint
+from .rfc6749.endpoints import MetadataEndpoint
from .rfc6749.endpoints import TokenEndpoint
from .rfc6749.endpoints import ResourceEndpoint
from .rfc6749.endpoints import RevocationEndpoint
diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py
index 9557f92..51e173d 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py
@@ -10,6 +10,7 @@ from __future__ import absolute_import, unicode_literals
from .authorization import AuthorizationEndpoint
from .introspect import IntrospectEndpoint
+from .metadata import MetadataEndpoint
from .token import TokenEndpoint
from .resource import ResourceEndpoint
from .revocation import RevocationEndpoint
diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
new file mode 100644
index 0000000..6d77b9f
--- /dev/null
+++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.endpoint.metadata
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the `OAuth 2.0 Authorization Server Metadata`.
+
+.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414
+"""
+from __future__ import absolute_import, unicode_literals
+
+import copy
+import json
+import logging
+
+from ....common import unicode_type
+from .base import BaseEndpoint, catch_errors_and_unavailability
+from .authorization import AuthorizationEndpoint
+from .introspect import IntrospectEndpoint
+from .token import TokenEndpoint
+from .revocation import RevocationEndpoint
+
+
+log = logging.getLogger(__name__)
+
+
+class MetadataEndpoint(BaseEndpoint):
+
+ """OAuth2.0 Authorization Server Metadata endpoint.
+
+ This specification generalizes the metadata format defined by
+ `OpenID Connect Discovery 1.0` in a way that is compatible
+ with OpenID Connect Discovery while being applicable to a wider set
+ of OAuth 2.0 use cases. This is intentionally parallel to the way
+ that `OAuth 2.0 Dynamic Client Registration Protocol` [RFC7591]
+ generalized the dynamic client registration mechanisms defined by
+ `OpenID Connect Dynamic Client Registration 1.0`
+ in a way that is compatible with it.
+
+ .. _`OpenID Connect Discovery 1.0`: http://openid.net/specs/openid-connect-discovery-1_0.html
+ .. _`OAuth 2.0 Dynamic Client Registration Protocol`: https://tools.ietf.org/html/rfc7591
+ .. _`OpenID Connect Dynamic Client Registration 1.0`: https://openid.net/specs/openid-connect-registration-1_0.html
+ """
+
+ def __init__(self, endpoints, claims={}, raise_errors=True):
+ assert isinstance(claims, dict)
+ for endpoint in endpoints:
+ assert isinstance(endpoint, BaseEndpoint)
+
+ BaseEndpoint.__init__(self)
+ self.raise_errors = raise_errors
+ self.endpoints = endpoints
+ self.initial_claims = claims
+ self.claims = self.validate_metadata_server()
+
+ @catch_errors_and_unavailability
+ def create_metadata_response(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Create metadata response
+ """
+ headers = {
+ 'Content-Type': 'application/json'
+ }
+ return headers, json.dumps(self.claims), 200
+
+ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False):
+ if not self.raise_errors:
+ return
+
+ if key not in array:
+ if is_required:
+ raise ValueError("key {} is a mandatory metadata.".format(key))
+
+ elif is_issuer:
+ if not array[key].startswith("https"):
+ raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key]))
+ if "?" in array[key] or "&" in array[key] or "#" in array[key]:
+ raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key]))
+
+ elif is_url:
+ if not array[key].startswith("http"):
+ raise ValueError("key {}: {} must be an URL".format(key, array[key]))
+
+ elif is_list:
+ if not isinstance(array[key], list):
+ raise ValueError("key {}: {} must be an Array".format(key, array[key]))
+ for elem in array[key]:
+ if not isinstance(elem, unicode_type):
+ raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem))
+
+ def validate_metadata_token(self, claims, endpoint):
+ claims.setdefault("grant_types_supported", list(endpoint._grant_types.keys()))
+ claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "grant_types_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_authorization(self, claims, endpoint):
+ claims.setdefault("response_types_supported", list(self._response_types.keys()))
+ claims.setdefault("response_modes_supported", ["query", "fragment"])
+
+ self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True)
+ self.validate_metadata(claims, "response_modes_supported", is_list=True)
+ if "code" in claims["response_types_supported"]:
+ self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True)
+ self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_revocation(self, claims, endpoint):
+ claims.setdefault("revocation_endpoint_auth_methods_supported",
+ ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_introspection(self, claims, endpoint):
+ claims.setdefault("introspection_endpoint_auth_methods_supported",
+ ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_server(self):
+ """
+ Authorization servers can have metadata describing their
+ configuration. The following authorization server metadata values
+ are used by this specification. More details can be found in `RFC8414` :
+
+ issuer
+ REQUIRED
+
+ authorization_endpoint
+ URL of the authorization server's authorization endpoint
+ [RFC6749]. This is REQUIRED unless no grant types are supported
+ that use the authorization endpoint.
+
+ token_endpoint
+ URL of the authorization server's token endpoint [RFC6749]. This
+ is REQUIRED unless only the implicit grant type is supported.
+
+ scopes_supported
+ RECOMMENDED.
+
+ response_types_supported
+ REQUIRED.
+
+ * Other OPTIONAL fields:
+ jwks_uri
+ registration_endpoint
+ 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
+
+ Additional authorization server metadata parameters MAY also be used.
+ Some are defined by other specifications, such as OpenID Connect
+ Discovery 1.0 [OpenID.Discovery].
+
+ .. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2
+ """
+ claims = copy.deepcopy(self.initial_claims)
+ self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True)
+ self.validate_metadata(claims, "jwks_uri", is_url=True)
+ self.validate_metadata(claims, "scopes_supported", is_list=True)
+ self.validate_metadata(claims, "service_documentation", is_url=True)
+ self.validate_metadata(claims, "ui_locales_supported", is_list=True)
+ self.validate_metadata(claims, "op_policy_uri", is_url=True)
+ self.validate_metadata(claims, "op_tos_uri", is_url=True)
+
+ for endpoint in self.endpoints:
+ if isinstance(endpoint, TokenEndpoint):
+ self.validate_metadata_token(claims, endpoint)
+ if isinstance(endpoint, AuthorizationEndpoint):
+ self.validate_metadata_authorization(claims, endpoint)
+ if isinstance(endpoint, RevocationEndpoint):
+ self.validate_metadata_revocation(claims, endpoint)
+ if isinstance(endpoint, IntrospectEndpoint):
+ self.validate_metadata_introspection(claims, endpoint)
+ return claims
diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py
new file mode 100644
index 0000000..301e846
--- /dev/null
+++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+from oauthlib.oauth2 import MetadataEndpoint
+from oauthlib.oauth2 import TokenEndpoint
+
+from ....unittest import TestCase
+
+
+class MetadataEndpointTest(TestCase):
+ def setUp(self):
+ self.metadata = {
+ "issuer": 'https://foo.bar'
+ }
+
+ def test_token_endpoint(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token"
+ })
+ self.assertIn("grant_types_supported", metadata.claims)
+ self.assertEqual(metadata.claims["grant_types_supported"], ["password"])
+
+ def test_token_endpoint_overridden(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token",
+ "grant_types_supported": ["pass_word_special_provider"]
+ })
+ self.assertIn("grant_types_supported", metadata.claims)
+ self.assertEqual(metadata.claims["grant_types_supported"], ["pass_word_special_provider"])
+
+ def test_mandatory_fields(self):
+ metadata = MetadataEndpoint([], self.metadata)
+ self.assertIn("issuer", metadata.claims)
+ self.assertEqual(metadata.claims["issuer"], 'https://foo.bar')