oidc: add discovery document support
The OpenID Connect specifies that all providers must return a JSON discovery document [1] in a well-known location. We can let the user pass this document instead of the individual endpoints (i.e. token and authorization endpoint). Moreover, we can also check if the requested grant_type (implicit to the used plugin, and one of client_credentials, password, authorization_code) is supported by the provider before starting the auth flow. [1] https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata Fixes-bug: #1583682 Change-Id: I24b7960b25ddcff45552c0ab5541d92122d1d560
This commit is contained in:
parent
88d4fdb14a
commit
00746ea636
@ -18,5 +18,6 @@ from keystoneauth1.exceptions.catalog import * # noqa
|
||||
from keystoneauth1.exceptions.connection import * # noqa
|
||||
from keystoneauth1.exceptions.discovery import * # noqa
|
||||
from keystoneauth1.exceptions.http import * # noqa
|
||||
from keystoneauth1.exceptions.oidc import * # noqa
|
||||
from keystoneauth1.exceptions.response import * # noqa
|
||||
from keystoneauth1.exceptions.service_providers import * # noqa
|
||||
|
40
keystoneauth1/exceptions/oidc.py
Normal file
40
keystoneauth1/exceptions/oidc.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from keystoneauth1.exceptions import auth_plugins
|
||||
|
||||
__all__ = (
|
||||
'InvalidDiscoveryEndpoint', 'InvalidOidcDiscoveryDocument',
|
||||
'OidcAccessTokenEndpointNotFound', 'OidcAuthorizationEndpointNotFound',
|
||||
'OidcPluginNotSupported',
|
||||
)
|
||||
|
||||
|
||||
class InvalidDiscoveryEndpoint(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect Discovery Document endpoint not set."""
|
||||
|
||||
|
||||
class InvalidOidcDiscoveryDocument(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect Discovery Document is not valid JSON."""
|
||||
|
||||
|
||||
class OidcAccessTokenEndpointNotFound(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect access token endpoint not provided."
|
||||
|
||||
|
||||
class OidcAuthorizationEndpointNotFound(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect authorization endpoint not provided."
|
||||
|
||||
|
||||
class OidcPluginNotSupported(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect grant type not supported by provider."
|
@ -12,9 +12,13 @@
|
||||
|
||||
from positional import positional
|
||||
|
||||
from keystoneauth1 import _utils as utils
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.identity.v3 import federation
|
||||
|
||||
_logger = utils.get_logger(__name__)
|
||||
|
||||
__all__ = ('OidcAuthorizationCode',
|
||||
'OidcPassword',
|
||||
'OidcAccessToken')
|
||||
@ -28,9 +32,11 @@ class _OidcBase(federation.FederationBaseAuth):
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret, access_token_endpoint,
|
||||
grant_type, access_token_type,
|
||||
scope="openid profile", **kwargs):
|
||||
client_id, client_secret, grant_type,
|
||||
access_token_type,
|
||||
scope="openid profile",
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None, **kwargs):
|
||||
"""The OpenID Connect plugin expects the following.
|
||||
|
||||
:param auth_url: URL of the Identity Service
|
||||
@ -49,15 +55,12 @@ class _OidcBase(federation.FederationBaseAuth):
|
||||
:param client_secret: OAuth 2.0 Client Secret
|
||||
:type client_secret: string
|
||||
|
||||
:param access_token_endpoint: OpenID Connect Provider Token Endpoint,
|
||||
for example:
|
||||
https://localhost:8020/oidc/OP/token
|
||||
:type access_token_endpoint: string
|
||||
|
||||
:param grant_type: OpenID Connect grant type, it represents the flow
|
||||
that is used to talk to the OP. Valid values are:
|
||||
"authorization_code", "refresh_token", or
|
||||
"password".
|
||||
"password". If a discovery document is being used,
|
||||
this class will check if the provided value is
|
||||
supported by the provider.
|
||||
:type grant_type: string
|
||||
|
||||
:param access_token_type: OAuth 2.0 Authorization Server Introspection
|
||||
@ -67,6 +70,19 @@ class _OidcBase(federation.FederationBaseAuth):
|
||||
"access_token" or "id_token"
|
||||
:type access_token_type: string
|
||||
|
||||
:param access_token_endpoint: OpenID Connect Provider Token Endpoint,
|
||||
for example:
|
||||
https://localhost:8020/oidc/OP/token
|
||||
Note that if a discovery document is
|
||||
provided this value will override
|
||||
the discovered one.
|
||||
:type access_token_endpoint: string
|
||||
|
||||
:param discovery_endpoint: OpenID Connect Discovery Document URL,
|
||||
for example:
|
||||
https://localhost:8020/oidc/.well-known/openid-configuration
|
||||
:type access_token_endpoint: string
|
||||
|
||||
:param scope: OpenID Connect scope that is requested from OP,
|
||||
for example: "openid profile email", defaults to
|
||||
"openid profile". Note that OpenID Connect specification
|
||||
@ -77,11 +93,94 @@ class _OidcBase(federation.FederationBaseAuth):
|
||||
**kwargs)
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
self.discovery_endpoint = discovery_endpoint
|
||||
self._discovery_document = {}
|
||||
self.access_token_endpoint = access_token_endpoint
|
||||
self.grant_type = grant_type
|
||||
|
||||
self.access_token_type = access_token_type
|
||||
self.scope = scope
|
||||
|
||||
self.grant_type = grant_type
|
||||
|
||||
def _get_discovery_document(self, session):
|
||||
"""Get the contents of the OpenID Connect Discovery Document.
|
||||
|
||||
This method grabs the contents of the OpenID Connect Discovery Document
|
||||
if a discovery_endpoint was passed to the constructor and returns it as
|
||||
a dict, otherwise returns an empty dict. Note that it will fetch the
|
||||
discovery document only once, so subsequent calls to this method will
|
||||
return the cached result, if any.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: a python dictionary containing the discovery document if any,
|
||||
otherwise it will return an empty dict.
|
||||
:rtype: dict
|
||||
"""
|
||||
if (self.discovery_endpoint is not None and
|
||||
not self._discovery_document):
|
||||
try:
|
||||
resp = session.get(self.discovery_endpoint,
|
||||
authenticated=False)
|
||||
except exceptions.HttpError:
|
||||
_logger.error("Cannot fetch discovery document %(discovery)s" %
|
||||
{"discovery": self.discovery_endpoint})
|
||||
raise
|
||||
|
||||
try:
|
||||
self._discovery_document = resp.json()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not self._discovery_document:
|
||||
raise exceptions.InvalidOidcDiscoveryDocument()
|
||||
|
||||
return self._discovery_document
|
||||
|
||||
def _check_grant_type(self, session):
|
||||
"""Check if the grant_type requested is supported by the server.
|
||||
|
||||
If a discovery_endpoint is provided and the discoverty document
|
||||
advertises the supported grant types, this method will check if the
|
||||
requested grant_type is supported by the server, raising an exception
|
||||
otherwise.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
"""
|
||||
discovery = self._get_discovery_document(session)
|
||||
grant_types = discovery.get("grant_types_supported")
|
||||
if (grant_types and
|
||||
self.grant_type is not None and
|
||||
self.grant_type not in grant_types):
|
||||
raise exceptions.OidcPluginNotSupported()
|
||||
|
||||
def _get_access_token_endpoint(self, session):
|
||||
"""Get the "token_endpoint" for the OpenID Connect flow.
|
||||
|
||||
This method will return the correct access token endpoint to be used.
|
||||
If the user has explicitly passed an access_token_endpoint to the
|
||||
constructor that will be returned. If there is no explicit endpoint and
|
||||
a discovery url is provided, it will try to get it from the discovery
|
||||
document. If nothing is found, an exception will be raised.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:return: the endpoint to use
|
||||
:rtype: string or None if no endpoint is found
|
||||
"""
|
||||
if self.access_token_endpoint is not None:
|
||||
return self.access_token_endpoint
|
||||
|
||||
discovery = self._get_discovery_document(session)
|
||||
endpoint = discovery.get("token_endpoint")
|
||||
if endpoint is None:
|
||||
raise exceptions.OidcAccessTokenEndpointNotFound()
|
||||
return endpoint
|
||||
|
||||
def _get_access_token(self, session, payload):
|
||||
"""Exchange a variety of user supplied values for an access token.
|
||||
|
||||
@ -95,7 +194,9 @@ class _OidcBase(federation.FederationBaseAuth):
|
||||
:type payload: dict
|
||||
"""
|
||||
client_auth = (self.client_id, self.client_secret)
|
||||
op_response = session.post(self.access_token_endpoint,
|
||||
access_token_endpoint = self._get_access_token_endpoint(session)
|
||||
|
||||
op_response = session.post(access_token_endpoint,
|
||||
requests_auth=client_auth,
|
||||
data=payload,
|
||||
authenticated=False)
|
||||
@ -132,7 +233,9 @@ class OidcPassword(_OidcBase):
|
||||
|
||||
@positional(4)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret, access_token_endpoint,
|
||||
client_id, client_secret,
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None,
|
||||
grant_type='password', access_token_type='access_token',
|
||||
username=None, password=None, **kwargs):
|
||||
"""The OpenID Password plugin expects the following.
|
||||
@ -150,6 +253,7 @@ class OidcPassword(_OidcBase):
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
access_token_endpoint=access_token_endpoint,
|
||||
discovery_endpoint=discovery_endpoint,
|
||||
grant_type=grant_type,
|
||||
access_token_type=access_token_type,
|
||||
**kwargs)
|
||||
@ -176,6 +280,9 @@ class OidcPassword(_OidcBase):
|
||||
:returns: a token data representation
|
||||
:rtype: :py:class:`keystoneauth1.access.AccessInfoV3`
|
||||
"""
|
||||
# First of all, check if the grant type is supported
|
||||
self._check_grant_type(session)
|
||||
|
||||
# get an access token
|
||||
payload = {'grant_type': self.grant_type, 'username': self.username,
|
||||
'password': self.password, 'scope': self.scope}
|
||||
@ -192,7 +299,9 @@ class OidcAuthorizationCode(_OidcBase):
|
||||
|
||||
@positional(4)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret, access_token_endpoint,
|
||||
client_id, client_secret,
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None,
|
||||
grant_type='authorization_code',
|
||||
access_token_type='access_token',
|
||||
redirect_uri=None, code=None, **kwargs):
|
||||
@ -212,6 +321,7 @@ class OidcAuthorizationCode(_OidcBase):
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
access_token_endpoint=access_token_endpoint,
|
||||
discovery_endpoint=discovery_endpoint,
|
||||
grant_type=grant_type,
|
||||
access_token_type=access_token_type,
|
||||
**kwargs)
|
||||
@ -237,6 +347,9 @@ class OidcAuthorizationCode(_OidcBase):
|
||||
:returns: a token data representation
|
||||
:rtype: :py:class:`keystoneauth1.access.AccessInfoV3`
|
||||
"""
|
||||
# First of all, check if the grant type is supported
|
||||
self._check_grant_type(session)
|
||||
|
||||
# get an access token
|
||||
payload = {'grant_type': self.grant_type,
|
||||
'redirect_uri': self.redirect_uri,
|
||||
|
@ -78,6 +78,15 @@ class Token(loading.BaseV3Loader):
|
||||
|
||||
class _OpenIDConnectBase(loading.BaseFederationLoader):
|
||||
|
||||
def load_from_options(self, **kwargs):
|
||||
if not (kwargs.get('access_token_endpoint') or
|
||||
kwargs.get('discovery_endpoint')):
|
||||
m = ("You have to specify either an 'access-token-endpoint' or "
|
||||
"a 'discovery-endpoint'.")
|
||||
raise exceptions.OptionError(m)
|
||||
|
||||
return super(_OpenIDConnectBase, self).load_from_options(**kwargs)
|
||||
|
||||
def get_options(self):
|
||||
options = super(_OpenIDConnectBase, self).get_options()
|
||||
|
||||
@ -92,7 +101,17 @@ class _OpenIDConnectBase(loading.BaseFederationLoader):
|
||||
'Connect specification states that "openid" '
|
||||
'must be always specified.'),
|
||||
loading.Opt('access-token-endpoint',
|
||||
help='OpenID Connect Provider Token Endpoint'),
|
||||
help='OpenID Connect Provider Token Endpoint. Note '
|
||||
'that if a discovery document is being passed this '
|
||||
'option will override the endpoint provided by the '
|
||||
'server in the discovery document.'),
|
||||
loading.Opt('discovery-endpoint',
|
||||
help='OpenID Connect Discovery Document URL. '
|
||||
'The discovery document will be used to obtain the '
|
||||
'values of the access token endpoint and the '
|
||||
'authentication endpoint. This URL should look like '
|
||||
'https://idp.example.org/.well-known/'
|
||||
'openid-configuration'),
|
||||
loading.Opt('access-token-type',
|
||||
help='OAuth 2.0 Authorization Server Introspection '
|
||||
'token type, it is used to decide which type '
|
||||
|
@ -14,6 +14,7 @@ import uuid
|
||||
|
||||
from six.moves import urllib
|
||||
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.identity.v3 import oidc
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1.tests.unit import oidc_fixtures
|
||||
@ -23,7 +24,7 @@ from keystoneauth1.tests.unit import utils
|
||||
KEYSTONE_TOKEN_VALUE = uuid.uuid4().hex
|
||||
|
||||
|
||||
class BaseOIDCTests(utils.TestCase):
|
||||
class BaseOIDCTests(object):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseOIDCTests, self).setUp()
|
||||
@ -45,8 +46,113 @@ class BaseOIDCTests(utils.TestCase):
|
||||
self.REDIRECT_URL = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
self.CODE = '4/M9TNz2G9WVwYxSjx0w9AgA1bOmryJltQvOhQMq0czJs.cnLNVAfqwG'
|
||||
|
||||
self.DISCOVERY_URL = ('https://localhost:8020/oidc/.well-known/'
|
||||
'openid-configuration')
|
||||
|
||||
class OIDCPasswordTests(BaseOIDCTests):
|
||||
def test_discovery_not_found(self):
|
||||
self.requests_mock.get("http://not.found",
|
||||
status_code=404)
|
||||
|
||||
plugin = self.plugin.__class__(
|
||||
self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
discovery_endpoint="http://not.found")
|
||||
|
||||
self.assertRaises(exceptions.http.NotFound,
|
||||
plugin._get_discovery_document,
|
||||
self.session)
|
||||
|
||||
def test_no_discovery(self):
|
||||
|
||||
plugin = self.plugin.__class__(
|
||||
self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT,
|
||||
)
|
||||
self.assertEqual(self.ACCESS_TOKEN_ENDPOINT,
|
||||
plugin.access_token_endpoint)
|
||||
|
||||
def test_load_discovery(self):
|
||||
self.requests_mock.get(self.DISCOVERY_URL,
|
||||
json=oidc_fixtures.DISCOVERY_DOCUMENT)
|
||||
|
||||
plugin = self.plugin.__class__(self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
discovery_endpoint=self.DISCOVERY_URL)
|
||||
self.assertEqual(
|
||||
oidc_fixtures.DISCOVERY_DOCUMENT["token_endpoint"],
|
||||
plugin._get_access_token_endpoint(self.session)
|
||||
)
|
||||
|
||||
def test_no_access_token_endpoint(self):
|
||||
plugin = self.plugin.__class__(self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET)
|
||||
|
||||
self.assertRaises(exceptions.OidcAccessTokenEndpointNotFound,
|
||||
plugin._get_access_token_endpoint,
|
||||
self.session)
|
||||
|
||||
def test_invalid_discovery_document(self):
|
||||
self.requests_mock.get(self.DISCOVERY_URL,
|
||||
json={})
|
||||
|
||||
plugin = self.plugin.__class__(self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
discovery_endpoint=self.DISCOVERY_URL)
|
||||
|
||||
self.assertRaises(exceptions.InvalidOidcDiscoveryDocument,
|
||||
plugin._get_discovery_document,
|
||||
self.session)
|
||||
|
||||
def test_load_discovery_override_by_endpoints(self):
|
||||
self.requests_mock.get(self.DISCOVERY_URL,
|
||||
json=oidc_fixtures.DISCOVERY_DOCUMENT)
|
||||
|
||||
access_token_endpoint = uuid.uuid4().hex
|
||||
plugin = self.plugin.__class__(
|
||||
self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
discovery_endpoint=self.DISCOVERY_URL,
|
||||
access_token_endpoint=access_token_endpoint
|
||||
)
|
||||
self.assertEqual(access_token_endpoint,
|
||||
plugin._get_access_token_endpoint(self.session))
|
||||
|
||||
def test_wrong_grant_type(self):
|
||||
self.requests_mock.get(self.DISCOVERY_URL,
|
||||
json={"grant_types_supported": ["foo", "bar"]})
|
||||
|
||||
plugin = self.plugin.__class__(self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
discovery_endpoint=self.DISCOVERY_URL)
|
||||
|
||||
self.assertRaises(exceptions.OidcPluginNotSupported,
|
||||
plugin._check_grant_type,
|
||||
self.session)
|
||||
|
||||
|
||||
class OIDCPasswordTests(BaseOIDCTests, utils.TestCase):
|
||||
def setUp(self):
|
||||
super(OIDCPasswordTests, self).setUp()
|
||||
|
||||
@ -118,7 +224,7 @@ class OIDCPasswordTests(BaseOIDCTests):
|
||||
self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token)
|
||||
|
||||
|
||||
class OIDCAuthorizationGrantTests(BaseOIDCTests):
|
||||
class OIDCAuthorizationGrantTests(BaseOIDCTests, utils.TestCase):
|
||||
def setUp(self):
|
||||
super(OIDCAuthorizationGrantTests, self).setUp()
|
||||
|
||||
|
@ -135,7 +135,8 @@ class OpenIDConnectBaseTests(object):
|
||||
options = loading.get_plugin_loader(self.plugin_name).get_options()
|
||||
self.assertTrue(
|
||||
set(['client-id', 'client-secret', 'access-token-endpoint',
|
||||
'access-token-type', 'openid-scope']).issubset(
|
||||
'access-token-type', 'openid-scope',
|
||||
'discovery-endpoint']).issubset(
|
||||
set([o.name for o in options]))
|
||||
)
|
||||
# openid-scope gets renamed into "scope"
|
||||
|
@ -49,3 +49,50 @@ ACCESS_TOKEN_VIA_AUTH_GRANT_RESP = {
|
||||
"refresh_token": "1/ySXNO9XISBMIgOrJDtdun6zK6XiATCKT",
|
||||
"id_token": "eyJhbGciOiJSUzI1Ni8hOYHuZT8dt_yynmJVhcU"
|
||||
}
|
||||
|
||||
DISCOVERY_DOCUMENT = {
|
||||
"authorization_endpoint": "https://localhost:8020/oidc/authorize",
|
||||
"claims_supported": [
|
||||
"sub",
|
||||
"name",
|
||||
"preferred_username",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"middle_name",
|
||||
"nickname",
|
||||
"profile",
|
||||
"picture",
|
||||
"website",
|
||||
"gender",
|
||||
"zoneinfo",
|
||||
"locale",
|
||||
"updated_at",
|
||||
"birthdate",
|
||||
"email",
|
||||
"email_verified",
|
||||
"phone_number",
|
||||
"phone_number_verified",
|
||||
"address"
|
||||
],
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"password",
|
||||
],
|
||||
"introspection_endpoint": "https://localhost:8020/oidc/introspect",
|
||||
"issuer": "https://localhost:8020/oidc/",
|
||||
"jwks_uri": "https://localhost:8020/oidc/jwk",
|
||||
"op_policy_uri": "https://localhost:8020/oidc/about",
|
||||
"op_tos_uri": "https://localhost:8020/oidc/about",
|
||||
"registration_endpoint": "https://localhost:8020/oidc/register",
|
||||
"revocation_endpoint": "https://localhost:8020/oidc/revoke",
|
||||
"service_documentation": "https://localhost:8020/oidc/about",
|
||||
"token_endpoint": "https://localhost:8020/oidc/token",
|
||||
"userinfo_endpoint": "https://localhost:8020/oidc/userinfo",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none"
|
||||
],
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
features:
|
||||
- >
|
||||
Add support for the `OpenID Connect Discovery Document
|
||||
<https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>`_
|
||||
into the OpenID Connect related plugins. Now it is possible to only pass the
|
||||
`discovery-url` option and the plugins will try to fetch the required
|
||||
metadata from there.
|
||||
fixes:
|
||||
- >
|
||||
[`bug 1583682 <https://bugs.launchpad.net/keystoneauth/+bug/1583682>`_]
|
||||
OpenID Connect plugins should support OpenID Connect Discovery.
|
Loading…
x
Reference in New Issue
Block a user