Merge "SAML2 ECP auth plugin"
This commit is contained in:
0
keystoneclient/contrib/auth/__init__.py
Normal file
0
keystoneclient/contrib/auth/__init__.py
Normal file
0
keystoneclient/contrib/auth/v3/__init__.py
Normal file
0
keystoneclient/contrib/auth/v3/__init__.py
Normal file
411
keystoneclient/contrib/auth/v3/saml2.py
Normal file
411
keystoneclient/contrib/auth/v3/saml2.py
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
# 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 lxml import etree
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from keystoneclient import access
|
||||||
|
from keystoneclient.auth.identity import v3
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class Saml2UnscopedTokenAuthMethod(v3.AuthMethod):
|
||||||
|
_method_parameters = []
|
||||||
|
|
||||||
|
def get_auth_data(self, session, auth, headers, **kwargs):
|
||||||
|
raise exceptions.MethodNotImplemented(('This method should never '
|
||||||
|
'be called'))
|
||||||
|
|
||||||
|
|
||||||
|
class Saml2UnscopedToken(v3.AuthConstructor):
|
||||||
|
"""Implement authentication plugin for SAML2 protocol.
|
||||||
|
|
||||||
|
ECP stands for ``Enhanced Client or Proxy`` and is a SAML2 extension
|
||||||
|
for federated authentication where a transportation layer consists of
|
||||||
|
HTTP protocol and XML SOAP messages.
|
||||||
|
|
||||||
|
Read for more information::
|
||||||
|
``https://wiki.shibboleth.net/confluence/display/SHIB2/ECP``
|
||||||
|
|
||||||
|
The SAML2 ECP specification can be found at::
|
||||||
|
``https://www.oasis-open.org/committees/download.php/
|
||||||
|
49979/saml-ecp-v2.0-wd09.pdf``
|
||||||
|
|
||||||
|
Currently only HTTPBasicAuth mechanism is available for the IdP
|
||||||
|
authenication.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_auth_method_class = Saml2UnscopedTokenAuthMethod
|
||||||
|
|
||||||
|
PROTOCOL = 'saml2'
|
||||||
|
HTTP_MOVED_TEMPORARILY = 302
|
||||||
|
SAML2_HEADER_INDEX = 0
|
||||||
|
ECP_SP_EMPTY_REQUEST_HEADERS = {
|
||||||
|
'Accept': 'text/html; application/vnd.paos+xml',
|
||||||
|
'PAOS': ('ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:'
|
||||||
|
'SAML:2.0:profiles:SSO:ecp"')
|
||||||
|
}
|
||||||
|
|
||||||
|
ECP_SP_SAML2_REQUEST_HEADERS = {
|
||||||
|
'Content-Type': 'application/vnd.paos+xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
ECP_SAML2_NAMESPACES = {
|
||||||
|
'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp',
|
||||||
|
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||||
|
'paos': 'urn:liberty:paos:2003-08'
|
||||||
|
}
|
||||||
|
|
||||||
|
ECP_RELAY_STATE = '//ecp:RelayState'
|
||||||
|
|
||||||
|
ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:Request/'
|
||||||
|
'@responseConsumerURL')
|
||||||
|
|
||||||
|
ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/'
|
||||||
|
'@AssertionConsumerServiceURL')
|
||||||
|
|
||||||
|
SOAP_FAULT = """
|
||||||
|
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
|
<S:Body>
|
||||||
|
<S:Fault>
|
||||||
|
<faultcode>S:Server</faultcode>
|
||||||
|
<faultstring>responseConsumerURL from SP and
|
||||||
|
assertionConsumerServiceURL from IdP do not match
|
||||||
|
</faultstring>
|
||||||
|
</S:Fault>
|
||||||
|
</S:Body>
|
||||||
|
</S:Envelope>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_url,
|
||||||
|
identity_provider,
|
||||||
|
identity_provider_url,
|
||||||
|
username, password,
|
||||||
|
**kwargs):
|
||||||
|
"""Class constructor accepting following parameters:
|
||||||
|
:param auth_url: URL of the Identity Service
|
||||||
|
:type auth_url: string
|
||||||
|
|
||||||
|
:param identity_provider: name of the Identity Provider the client
|
||||||
|
will authenticate against. This parameter
|
||||||
|
will be used to build a dynamic URL used to
|
||||||
|
obtain unscoped OpenStack token.
|
||||||
|
:type identity_provider: string
|
||||||
|
|
||||||
|
:param identity_provider_url: An Identity Provider URL, where the SAML2
|
||||||
|
authn request will be sent.
|
||||||
|
:type identity_provider_url: string
|
||||||
|
|
||||||
|
:param username: User's login
|
||||||
|
:type username: string
|
||||||
|
|
||||||
|
:param password: User's password
|
||||||
|
:type password: string
|
||||||
|
|
||||||
|
"""
|
||||||
|
super(Saml2UnscopedToken, self).__init__(auth_url=auth_url, **kwargs)
|
||||||
|
self.identity_provider = identity_provider
|
||||||
|
self.identity_provider_url = identity_provider_url
|
||||||
|
self.username, self.password = username, password
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_options(cls):
|
||||||
|
options = super(Saml2UnscopedToken, cls).get_options()
|
||||||
|
options.extend([
|
||||||
|
cfg.StrOpt('identity-provider', help="Identity Provider's name"),
|
||||||
|
cfg.StrOpt('identity-provider-url',
|
||||||
|
help="Identity Provider's URL"),
|
||||||
|
cfg.StrOpt('user-name', dest='username', help='Username',
|
||||||
|
deprecated_name='username'),
|
||||||
|
cfg.StrOpt('password', help='Password')
|
||||||
|
])
|
||||||
|
return options
|
||||||
|
|
||||||
|
def _handle_http_302_ecp_redirect(self, session, response, method,
|
||||||
|
**kwargs):
|
||||||
|
if response.status_code != self.HTTP_MOVED_TEMPORARILY:
|
||||||
|
return response
|
||||||
|
|
||||||
|
location = response.headers['location']
|
||||||
|
return session.request(location, method, **kwargs)
|
||||||
|
|
||||||
|
def _first(self, _list):
|
||||||
|
if len(_list) != 1:
|
||||||
|
raise IndexError("Only single element list can be flatten")
|
||||||
|
return _list[0]
|
||||||
|
|
||||||
|
def _prepare_idp_saml2_request(self, saml2_authn_request):
|
||||||
|
header = saml2_authn_request[self.SAML2_HEADER_INDEX]
|
||||||
|
saml2_authn_request.remove(header)
|
||||||
|
|
||||||
|
def _check_consumer_urls(self, session, sp_response_consumer_url,
|
||||||
|
idp_sp_response_consumer_url):
|
||||||
|
"""Check if consumer URLs issued by SP and IdP are equal.
|
||||||
|
|
||||||
|
In the initial SAML2 authn Request issued by a Service Provider
|
||||||
|
there is a url called ``consumer url``. A trusted Identity Provider
|
||||||
|
should issue identical url. If the URLs are not equal the federated
|
||||||
|
authn process should be interrupted and the user should be warned.
|
||||||
|
|
||||||
|
:param session: session object to send out HTTP requests.
|
||||||
|
:type session: keystoneclient.session.Session
|
||||||
|
:param sp_response_consumer_url: consumer URL issued by a SP
|
||||||
|
:type sp_response_consumer_url: string
|
||||||
|
:param idp_sp_response_consumer_url: consumer URL issued by an IdP
|
||||||
|
:type idp_sp_response_consumer_url: string
|
||||||
|
|
||||||
|
"""
|
||||||
|
if sp_response_consumer_url != idp_sp_response_consumer_url:
|
||||||
|
# send fault message to the SP, discard the response
|
||||||
|
session.post(sp_response_consumer_url, data=self.SOAP_FAULT,
|
||||||
|
headers=self.ECP_SP_SAML2_REQUEST_HEADERS,
|
||||||
|
authenticated=False)
|
||||||
|
|
||||||
|
# prepare error message and raise an exception.
|
||||||
|
msg = ("Consumer URLs from Service Provider %(service_provider)s "
|
||||||
|
"%(sp_consumer_url)s and Identity Provider "
|
||||||
|
"%(identity_provider)s %(idp_consumer_url)s are not equal")
|
||||||
|
msg = msg % {
|
||||||
|
'service_provider': self.saml2_token_url,
|
||||||
|
'sp_consumer_url': sp_response_consumer_url,
|
||||||
|
'identity_provider': self.identity_provider,
|
||||||
|
'idp_consumer_url': idp_sp_response_consumer_url
|
||||||
|
}
|
||||||
|
|
||||||
|
raise exceptions.ValidationError(msg)
|
||||||
|
|
||||||
|
def _send_service_provider_request(self, session):
|
||||||
|
"""Initial HTTP GET request to the SAML2 protected endpoint.
|
||||||
|
|
||||||
|
It's crucial to include HTTP headers indicating that the client is
|
||||||
|
willing to take advantage of the ECP SAML2 extension and receive data
|
||||||
|
as the SOAP.
|
||||||
|
Unlike standard authentication methods in the OpenStack Identity,
|
||||||
|
the client accesses::
|
||||||
|
``/v3/OS-FEDERATION/identity_providers/{identity_providers}/
|
||||||
|
protocols/{protocol}/auth``
|
||||||
|
|
||||||
|
After a successful HTTP call the HTTP response should include SAML2
|
||||||
|
authn request in the XML format.
|
||||||
|
|
||||||
|
If a HTTP response contains ``X-Subject-Token`` in the headers and
|
||||||
|
the response body is a valid JSON assume the user was already
|
||||||
|
authenticated and Keystone returned a valid unscoped token.
|
||||||
|
Return True indicating the user was already authenticated.
|
||||||
|
|
||||||
|
:param session: a session object to send out HTTP requests.
|
||||||
|
:type session: keystoneclient.session.Session
|
||||||
|
|
||||||
|
"""
|
||||||
|
sp_response = session.get(self.saml2_token_url,
|
||||||
|
headers=self.ECP_SP_EMPTY_REQUEST_HEADERS,
|
||||||
|
authenticated=False)
|
||||||
|
|
||||||
|
if 'X-Subject-Token' in sp_response.headers:
|
||||||
|
self.authenticated_response = sp_response
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.saml2_authn_request = etree.XML(sp_response.content)
|
||||||
|
except etree.XMLSyntaxError as e:
|
||||||
|
msg = ("SAML2: Error parsing XML returned "
|
||||||
|
"from Service Provider, reason: %s" % e)
|
||||||
|
raise exceptions.AuthorizationFailure(msg)
|
||||||
|
|
||||||
|
relay_state = self.saml2_authn_request.xpath(
|
||||||
|
self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES)
|
||||||
|
self.relay_state = self._first(relay_state)
|
||||||
|
|
||||||
|
sp_response_consumer_url = self.saml2_authn_request.xpath(
|
||||||
|
self.ECP_SERVICE_PROVIDER_CONSUMER_URL,
|
||||||
|
namespaces=self.ECP_SAML2_NAMESPACES)
|
||||||
|
self.sp_response_consumer_url = self._first(
|
||||||
|
sp_response_consumer_url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _send_idp_saml2_authn_request(self, session):
|
||||||
|
"""Present modified SAML2 authn assertion from the Service Provider."""
|
||||||
|
|
||||||
|
self._prepare_idp_saml2_request(self.saml2_authn_request)
|
||||||
|
idp_saml2_authn_request = self.saml2_authn_request
|
||||||
|
|
||||||
|
# Currently HTTPBasicAuth method is hardcoded into the plugin
|
||||||
|
idp_response = session.post(
|
||||||
|
self.identity_provider_url,
|
||||||
|
headers={'Content-type': 'text/xml'},
|
||||||
|
data=etree.tostring(idp_saml2_authn_request),
|
||||||
|
requests_auth=(self.username, self.password))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.saml2_idp_authn_response = etree.XML(idp_response.content)
|
||||||
|
except etree.XMLSyntaxError as e:
|
||||||
|
msg = ("SAML2: Error parsing XML returned "
|
||||||
|
"from Identity Provider, reason: %s" % e)
|
||||||
|
raise exceptions.AuthorizationFailure(msg)
|
||||||
|
|
||||||
|
idp_response_consumer_url = self.saml2_idp_authn_response.xpath(
|
||||||
|
self.ECP_IDP_CONSUMER_URL,
|
||||||
|
namespaces=self.ECP_SAML2_NAMESPACES)
|
||||||
|
|
||||||
|
self.idp_response_consumer_url = self._first(
|
||||||
|
idp_response_consumer_url)
|
||||||
|
|
||||||
|
self._check_consumer_urls(session, self.idp_response_consumer_url,
|
||||||
|
self.sp_response_consumer_url)
|
||||||
|
|
||||||
|
def _send_service_provider_saml2_authn_response(self, session):
|
||||||
|
"""Present SAML2 assertion to the Service Provider.
|
||||||
|
|
||||||
|
The assertion is issued by a trusted Identity Provider for the
|
||||||
|
authenticated user. This function directs the HTTP request to SP
|
||||||
|
managed URL, for instance: ``https://<host>:<port>/Shibboleth.sso/
|
||||||
|
SAML2/ECP``.
|
||||||
|
Upon success the there's a session created and access to the protected
|
||||||
|
resource is granted. Many implementations of the SP return HTTP 302
|
||||||
|
status code pointing to the protected URL (``https://<host>:<port>/v3/
|
||||||
|
OS-FEDERATION/identity_providers/{identity_provider}/protocols/
|
||||||
|
{protocol_id}/auth`` in this case). Saml2 plugin should point to that
|
||||||
|
URL again, with HTTP GET method, expecting an unscoped token.
|
||||||
|
|
||||||
|
:param session: a session object to send out HTTP requests.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.saml2_idp_authn_response[0][0] = self.relay_state
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
self.idp_response_consumer_url,
|
||||||
|
headers=self.ECP_SP_SAML2_REQUEST_HEADERS,
|
||||||
|
data=etree.tostring(self.saml2_idp_authn_response),
|
||||||
|
authenticated=False, redirect=False)
|
||||||
|
|
||||||
|
# Don't follow HTTP specs - after the HTTP 302 response don't repeat
|
||||||
|
# the call directed to the Location URL. In this case, this is an
|
||||||
|
# indication that saml2 session is now active and protected resource
|
||||||
|
# can be accessed.
|
||||||
|
response = self._handle_http_302_ecp_redirect(
|
||||||
|
session, response, method='GET',
|
||||||
|
headers=self.ECP_SP_SAML2_REQUEST_HEADERS)
|
||||||
|
|
||||||
|
self.authenticated_response = response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def saml2_token_url(self):
|
||||||
|
"""Return full URL where authorization data is sent."""
|
||||||
|
values = {
|
||||||
|
'host': self.auth_url.rstrip('/'),
|
||||||
|
'identity_provider': self.identity_provider,
|
||||||
|
'protocol': self.PROTOCOL
|
||||||
|
}
|
||||||
|
url = ("%(host)s/OS-FEDERATION/identity_providers/"
|
||||||
|
"%(identity_provider)s/protocols/%(protocol)s/auth")
|
||||||
|
url = url % values
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
def _get_unscoped_token(self, session, **kwargs):
|
||||||
|
"""Get unscoped OpenStack token after federated authentication.
|
||||||
|
|
||||||
|
This is a multi-step process including multiple HTTP requests.
|
||||||
|
|
||||||
|
The federated authentication consists of::
|
||||||
|
* HTTP GET request to the Identity Service (acting as a Service
|
||||||
|
Provider). Client utilizes URL::
|
||||||
|
``/v3/OS-FEDERATION/identity_providers/{identity_provider}/
|
||||||
|
protocols/saml2/auth``.
|
||||||
|
It's crucial to include HTTP headers indicating we are expecting
|
||||||
|
SOAP message in return.
|
||||||
|
Service Provider should respond with such SOAP message.
|
||||||
|
This step is handed by a method
|
||||||
|
``Saml2UnscopedToken_send_service_provider_request()``
|
||||||
|
|
||||||
|
* HTTP POST request to the external Identity Provider service with
|
||||||
|
ECP extension enabled. The content sent is a header removed SOAP
|
||||||
|
message returned from the Service Provider. It's also worth noting
|
||||||
|
that ECP extension to the SAML2 doesn't define authentication method.
|
||||||
|
The most popular is HttpBasicAuth with just user and password.
|
||||||
|
Other possibilities could be X509 certificates or Kerberos.
|
||||||
|
Upon successful authentication the user should receive a SAML2
|
||||||
|
assertion.
|
||||||
|
This step is handed by a method
|
||||||
|
``Saml2UnscopedToken_send_idp_saml2_authn_request(session)``
|
||||||
|
|
||||||
|
* HTTP POST request again to the Service Provider. The body of the
|
||||||
|
request includes SAML2 assertion issued by a trusted Identity
|
||||||
|
Provider. The request should be sent to the Service Provider
|
||||||
|
consumer url specified in the SAML2 assertion.
|
||||||
|
Providing the authentication was successful and both Service Provider
|
||||||
|
and Identity Providers are trusted to each other, the Service
|
||||||
|
Provider will issue an unscoped token with a list of groups the
|
||||||
|
federated user is a member of.
|
||||||
|
This step is handed by a method
|
||||||
|
``Saml2UnscopedToken_send_service_provider_saml2_authn_response()``
|
||||||
|
|
||||||
|
Unscoped token example::
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": {
|
||||||
|
"methods": [
|
||||||
|
"saml2"
|
||||||
|
],
|
||||||
|
"user": {
|
||||||
|
"id": "username%40example.com",
|
||||||
|
"name": "username@example.com",
|
||||||
|
"OS-FEDERATION": {
|
||||||
|
"identity_provider": "ACME",
|
||||||
|
"protocol": "saml2",
|
||||||
|
"groups": [
|
||||||
|
{"id": "abc123"},
|
||||||
|
{"id": "bcd234"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:param session : a session object to send out HTTP requests.
|
||||||
|
:type session: keystoneclient.session.Session
|
||||||
|
|
||||||
|
:returns: (token, token_json)
|
||||||
|
|
||||||
|
"""
|
||||||
|
saml_authenticated = self._send_service_provider_request(session)
|
||||||
|
if not saml_authenticated:
|
||||||
|
self._send_idp_saml2_authn_request(session)
|
||||||
|
self._send_service_provider_saml2_authn_response(session)
|
||||||
|
return (self.authenticated_response.headers['X-Subject-Token'],
|
||||||
|
self.authenticated_response.json()['token'])
|
||||||
|
|
||||||
|
def get_auth_ref(self, session, **kwargs):
|
||||||
|
"""Authenticate via SAML2 protocol and retrieve unscoped token.
|
||||||
|
|
||||||
|
This is a multi-step process where a client does federated authn
|
||||||
|
receives an unscoped token.
|
||||||
|
|
||||||
|
Federated authentication utilizing SAML2 Enhanced Client or Proxy
|
||||||
|
extension. See ``Saml2UnscopedToken_get_unscoped_token()``
|
||||||
|
for more information on that step.
|
||||||
|
Upon successful authentication and assertion mapping an
|
||||||
|
unscoped token is returned and stored within the plugin object for
|
||||||
|
further use.
|
||||||
|
|
||||||
|
:param session : a session object to send out HTTP requests.
|
||||||
|
:type session: keystoneclient.session.Session
|
||||||
|
|
||||||
|
:return access.AccessInfoV3: an object with scoped token's id and
|
||||||
|
unscoped token json included.
|
||||||
|
|
||||||
|
"""
|
||||||
|
token, token_json = self._get_unscoped_token(session, **kwargs)
|
||||||
|
return access.AccessInfoV3(token,
|
||||||
|
**token_json)
|
||||||
121
keystoneclient/tests/v3/saml2_fixtures.py
Normal file
121
keystoneclient/tests/v3/saml2_fixtures.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
SP_SOAP_RESPONSE = b"""<S:Envelope
|
||||||
|
xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
|
<S:Header>
|
||||||
|
<paos:Request xmlns:paos="urn:liberty:paos:2003-08"
|
||||||
|
S:actor="http://schemas.xmlsoap.org/soap/actor/next"
|
||||||
|
S:mustUnderstand="1"
|
||||||
|
responseConsumerURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
|
||||||
|
service="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"/>
|
||||||
|
<ecp:Request xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
|
||||||
|
IsPassive="0" S:actor="http://schemas.xmlsoap.org/soap/actor/next"
|
||||||
|
S:mustUnderstand="1">
|
||||||
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||||
|
https://openstack4.local/shibboleth
|
||||||
|
</saml:Issuer>
|
||||||
|
<samlp:IDPList xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<samlp:IDPEntry ProviderID="https://idp.testshib.org/idp/shibboleth"/>
|
||||||
|
</samlp:IDPList></ecp:Request>
|
||||||
|
<ecp:RelayState xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
|
||||||
|
S:actor="http://schemas.xmlsoap.org/soap/actor/next" S:mustUnderstand="1">
|
||||||
|
ss:mem:6f1f20fafbb38433467e9d477df67615</ecp:RelayState>
|
||||||
|
</S:Header><S:Body><samlp:AuthnRequest
|
||||||
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||||
|
AssertionConsumerServiceURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
|
||||||
|
ID="_a07186e3992e70e92c17b9d249495643" IssueInstant="2014-06-09T09:48:57Z"
|
||||||
|
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Version="2.0">
|
||||||
|
<saml:Issuer
|
||||||
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||||
|
https://openstack4.local/shibboleth
|
||||||
|
</saml:Issuer><samlp:NameIDPolicy AllowCreate="1"/><samlp:Scoping>
|
||||||
|
<samlp:IDPList>
|
||||||
|
<samlp:IDPEntry ProviderID="https://idp.testshib.org/idp/shibboleth"/>
|
||||||
|
</samlp:IDPList></samlp:Scoping></samlp:AuthnRequest></S:Body></S:Envelope>"""
|
||||||
|
|
||||||
|
|
||||||
|
SAML2_ASSERTION = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
|
<soap11:Header>
|
||||||
|
<ecp:Response xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
|
||||||
|
AssertionConsumerServiceURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
|
||||||
|
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next"
|
||||||
|
soap11:mustUnderstand="1"/>
|
||||||
|
<samlec:GeneratedKey xmlns:samlec="urn:ietf:params:xml:ns:samlec"
|
||||||
|
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next">
|
||||||
|
x=
|
||||||
|
</samlec:GeneratedKey>
|
||||||
|
</soap11:Header>
|
||||||
|
<soap11:Body>
|
||||||
|
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||||
|
Destination="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
|
||||||
|
ID="_bbbe6298d7ee586c915d952013875440"
|
||||||
|
InResponseTo="_a07186e3992e70e92c17b9d249495643"
|
||||||
|
IssueInstant="2014-06-09T09:48:58.945Z" Version="2.0">
|
||||||
|
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||||
|
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
|
||||||
|
https://idp.testshib.org/idp/shibboleth
|
||||||
|
</saml2:Issuer><saml2p:Status>
|
||||||
|
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||||
|
</saml2p:Status>
|
||||||
|
<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||||
|
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
|
||||||
|
Id="_e5215ac77a6028a8da8caa8be89bad44"
|
||||||
|
Type="http://www.w3.org/2001/04/xmlenc#Element">
|
||||||
|
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"
|
||||||
|
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"/>
|
||||||
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<xenc:EncryptedKey Id="_204349856f6e73c9480afc949d1b4643"
|
||||||
|
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
|
||||||
|
<xenc:EncryptionMethod
|
||||||
|
Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"
|
||||||
|
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
|
||||||
|
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"
|
||||||
|
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>
|
||||||
|
</xenc:EncryptionMethod><ds:KeyInfo><ds:X509Data><ds:X509Certificate>
|
||||||
|
</ds:X509Certificate>
|
||||||
|
</ds:X509Data></ds:KeyInfo>
|
||||||
|
<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
|
||||||
|
<xenc:CipherValue>VALUE==</xenc:CipherValue></xenc:CipherData>
|
||||||
|
</xenc:EncryptedKey></ds:KeyInfo>
|
||||||
|
<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
|
||||||
|
<xenc:CipherValue>VALUE=</xenc:CipherValue></xenc:CipherData>
|
||||||
|
</xenc:EncryptedData></saml2:EncryptedAssertion></saml2p:Response>
|
||||||
|
</soap11:Body></soap11:Envelope>
|
||||||
|
"""
|
||||||
|
|
||||||
|
UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN'
|
||||||
|
|
||||||
|
UNSCOPED_TOKEN = {
|
||||||
|
"token": {
|
||||||
|
"issued_at": "2014-06-09T09:48:59.643406Z",
|
||||||
|
"extras": {},
|
||||||
|
"methods": ["saml2"],
|
||||||
|
"expires_at": "2014-06-09T10:48:59.643375Z",
|
||||||
|
"user": {
|
||||||
|
"OS-FEDERATION": {
|
||||||
|
"identity_provider": {
|
||||||
|
"id": "testshib"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"id": "saml2"
|
||||||
|
},
|
||||||
|
"groups": [
|
||||||
|
{"id": "1764fa5cf69a49a4918131de5ce4af9a"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": "testhib%20user",
|
||||||
|
"name": "testhib user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
311
keystoneclient/tests/v3/test_auth_saml2.py
Normal file
311
keystoneclient/tests/v3/test_auth_saml2.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import httpretty
|
||||||
|
from lxml import etree
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from keystoneclient.auth import conf
|
||||||
|
from keystoneclient.contrib.auth.v3 import saml2
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient.openstack.common.fixture import config
|
||||||
|
from keystoneclient.openstack.common import jsonutils
|
||||||
|
from keystoneclient import session
|
||||||
|
from keystoneclient.tests.auth import utils as auth_utils
|
||||||
|
from keystoneclient.tests.v3 import saml2_fixtures
|
||||||
|
from keystoneclient.tests.v3 import utils
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticateviaSAML2Tests(auth_utils.TestCase, utils.TestCase):
|
||||||
|
|
||||||
|
class _AuthenticatedResponse(object):
|
||||||
|
headers = {
|
||||||
|
'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER
|
||||||
|
}
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return saml2_fixtures.UNSCOPED_TOKEN
|
||||||
|
|
||||||
|
class _AuthenticatedResponseInvalidJson(_AuthenticatedResponse):
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
class _AuthentiatedResponseMissingTokenID(_AuthenticatedResponse):
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
utils.TestCase.setUp(self)
|
||||||
|
auth_utils.TestCase.setUp(self)
|
||||||
|
|
||||||
|
self.conf_fixture = auth_utils.TestCase.useFixture(self,
|
||||||
|
config.Config())
|
||||||
|
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||||
|
|
||||||
|
self.session = session.Session(auth=None, verify=False,
|
||||||
|
session=requests.Session())
|
||||||
|
self.ECP_SP_EMPTY_REQUEST_HEADERS = {
|
||||||
|
'Accept': 'text/html; application/vnd.paos+xml',
|
||||||
|
'PAOS': ('ver="urn:liberty:paos:2003-08";'
|
||||||
|
'"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"')
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ECP_SP_SAML2_REQUEST_HEADERS = {
|
||||||
|
'Content-Type': 'application/vnd.paos+xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ECP_SAML2_NAMESPACES = {
|
||||||
|
'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp',
|
||||||
|
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||||
|
'paos': 'urn:liberty:paos:2003-08'
|
||||||
|
}
|
||||||
|
self.ECP_RELAY_STATE = '//ecp:RelayState'
|
||||||
|
self.ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:'
|
||||||
|
'Request/'
|
||||||
|
'@responseConsumerURL')
|
||||||
|
self.ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/'
|
||||||
|
'@AssertionConsumerServiceURL')
|
||||||
|
self.IDENTITY_PROVIDER = 'testidp'
|
||||||
|
self.IDENTITY_PROVIDER_URL = 'http://local.url'
|
||||||
|
self.PROTOCOL = 'saml2'
|
||||||
|
self.FEDERATION_AUTH_URL = '%s/%s' % (
|
||||||
|
self.TEST_URL,
|
||||||
|
'OS-FEDERATION/identity_providers/testidp/protocols/saml2/auth')
|
||||||
|
self.SHIB_CONSUMER_URL = ('https://openstack4.local/'
|
||||||
|
'Shibboleth.sso/SAML2/ECP')
|
||||||
|
|
||||||
|
self.saml2plugin = saml2.Saml2UnscopedToken(
|
||||||
|
self.TEST_URL,
|
||||||
|
self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL,
|
||||||
|
self.TEST_USER, self.TEST_TOKEN)
|
||||||
|
|
||||||
|
def simple_http(self, method, url, body=b'', content_type=None,
|
||||||
|
headers=None, status=200, **kwargs):
|
||||||
|
self.stub_url(method, base_url=url, body=body, adding_headers=headers,
|
||||||
|
content_type=content_type, status=status, **kwargs)
|
||||||
|
|
||||||
|
def make_oneline(self, s):
|
||||||
|
return etree.tostring(etree.XML(s)).replace(b'\n', b'')
|
||||||
|
|
||||||
|
def test_conf_params(self):
|
||||||
|
section = uuid.uuid4().hex
|
||||||
|
identity_provider = uuid.uuid4().hex
|
||||||
|
identity_provider_url = uuid.uuid4().hex
|
||||||
|
username = uuid.uuid4().hex
|
||||||
|
password = uuid.uuid4().hex
|
||||||
|
self.conf_fixture.config(auth_section=section, group=self.GROUP)
|
||||||
|
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||||
|
|
||||||
|
self.conf_fixture.register_opts(saml2.Saml2UnscopedToken.get_options(),
|
||||||
|
group=section)
|
||||||
|
self.conf_fixture.config(auth_plugin='v3unscopedsaml',
|
||||||
|
identity_provider=identity_provider,
|
||||||
|
identity_provider_url=identity_provider_url,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
group=section)
|
||||||
|
|
||||||
|
a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
|
||||||
|
self.assertEqual(identity_provider, a.identity_provider)
|
||||||
|
self.assertEqual(identity_provider_url, a.identity_provider_url)
|
||||||
|
self.assertEqual(username, a.username)
|
||||||
|
self.assertEqual(password, a.password)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_initial_sp_call(self):
|
||||||
|
"""Test initial call, expect SOAP message."""
|
||||||
|
self.simple_http('GET', self.FEDERATION_AUTH_URL,
|
||||||
|
body=self.make_oneline(
|
||||||
|
saml2_fixtures.SP_SOAP_RESPONSE))
|
||||||
|
a = self.saml2plugin._send_service_provider_request(self.session)
|
||||||
|
|
||||||
|
self.assertFalse(a)
|
||||||
|
|
||||||
|
fixture_soap_response = self.make_oneline(
|
||||||
|
saml2_fixtures.SP_SOAP_RESPONSE)
|
||||||
|
|
||||||
|
sp_soap_response = self.make_oneline(
|
||||||
|
etree.tostring(self.saml2plugin.saml2_authn_request))
|
||||||
|
|
||||||
|
error_msg = "Expected %s instead of %s" % (fixture_soap_response,
|
||||||
|
sp_soap_response)
|
||||||
|
|
||||||
|
self.assertEqual(fixture_soap_response, sp_soap_response, error_msg)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.saml2plugin.sp_response_consumer_url, self.SHIB_CONSUMER_URL,
|
||||||
|
"Expected consumer_url set to %s instead of %s" % (
|
||||||
|
self.SHIB_CONSUMER_URL,
|
||||||
|
str(self.saml2plugin.sp_response_consumer_url)))
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_initial_sp_call_when_saml_authenticated(self):
|
||||||
|
|
||||||
|
headers = {'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}
|
||||||
|
self.simple_http('GET', self.FEDERATION_AUTH_URL,
|
||||||
|
body=jsonutils.dumps(saml2_fixtures.UNSCOPED_TOKEN),
|
||||||
|
headers=headers)
|
||||||
|
a = self.saml2plugin._send_service_provider_request(self.session)
|
||||||
|
self.assertTrue(a)
|
||||||
|
self.assertEqual(
|
||||||
|
saml2_fixtures.UNSCOPED_TOKEN['token'],
|
||||||
|
self.saml2plugin.authenticated_response.json()['token'])
|
||||||
|
self.assertEqual(
|
||||||
|
saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||||
|
self.saml2plugin.authenticated_response.headers['X-Subject-Token'])
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_get_unscoped_token_when_authenticated(self):
|
||||||
|
headers = {'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}
|
||||||
|
self.simple_http('GET', self.FEDERATION_AUTH_URL,
|
||||||
|
body=jsonutils.dumps(saml2_fixtures.UNSCOPED_TOKEN),
|
||||||
|
headers=headers)
|
||||||
|
token, token_body = self.saml2plugin._get_unscoped_token(self.session)
|
||||||
|
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_body)
|
||||||
|
|
||||||
|
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, token)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_initial_sp_call_invalid_response(self):
|
||||||
|
"""Send initial SP HTTP request and receive wrong server response."""
|
||||||
|
self.simple_http('GET', self.FEDERATION_AUTH_URL,
|
||||||
|
body="NON XML RESPONSE")
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.AuthorizationFailure,
|
||||||
|
self.saml2plugin._send_service_provider_request,
|
||||||
|
self.session)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_send_authn_req_to_idp(self):
|
||||||
|
self.simple_http('POST', self.IDENTITY_PROVIDER_URL,
|
||||||
|
body=saml2_fixtures.SAML2_ASSERTION)
|
||||||
|
|
||||||
|
self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL
|
||||||
|
self.saml2plugin.saml2_authn_request = etree.XML(
|
||||||
|
saml2_fixtures.SP_SOAP_RESPONSE)
|
||||||
|
self.saml2plugin._send_idp_saml2_authn_request(self.session)
|
||||||
|
|
||||||
|
idp_response = self.make_oneline(etree.tostring(
|
||||||
|
self.saml2plugin.saml2_idp_authn_response))
|
||||||
|
|
||||||
|
saml2_assertion_oneline = self.make_oneline(
|
||||||
|
saml2_fixtures.SAML2_ASSERTION)
|
||||||
|
error = "Expected %s instead of %s" % (saml2_fixtures.SAML2_ASSERTION,
|
||||||
|
idp_response)
|
||||||
|
self.assertEqual(idp_response, saml2_assertion_oneline, error)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_fail_basicauth_idp_authentication(self):
|
||||||
|
self.simple_http('POST', self.IDENTITY_PROVIDER_URL,
|
||||||
|
status=401)
|
||||||
|
|
||||||
|
self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL
|
||||||
|
self.saml2plugin.saml2_authn_request = etree.XML(
|
||||||
|
saml2_fixtures.SP_SOAP_RESPONSE)
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.Unauthorized,
|
||||||
|
self.saml2plugin._send_idp_saml2_authn_request,
|
||||||
|
self.session)
|
||||||
|
|
||||||
|
def test_mising_username_password_in_plugin(self):
|
||||||
|
self.assertRaises(TypeError,
|
||||||
|
saml2.Saml2UnscopedToken,
|
||||||
|
self.TEST_URL, self.IDENTITY_PROVIDER,
|
||||||
|
self.IDENTITY_PROVIDER_URL)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_send_authn_response_to_sp(self):
|
||||||
|
self.simple_http(
|
||||||
|
'POST', self.SHIB_CONSUMER_URL,
|
||||||
|
body=jsonutils.dumps(saml2_fixtures.UNSCOPED_TOKEN),
|
||||||
|
content_type='application/json',
|
||||||
|
status=200,
|
||||||
|
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
|
||||||
|
|
||||||
|
self.saml2plugin.relay_state = etree.XML(
|
||||||
|
saml2_fixtures.SP_SOAP_RESPONSE).xpath(
|
||||||
|
self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES)[0]
|
||||||
|
|
||||||
|
self.saml2plugin.saml2_idp_authn_response = etree.XML(
|
||||||
|
saml2_fixtures.SAML2_ASSERTION)
|
||||||
|
|
||||||
|
self.saml2plugin.idp_response_consumer_url = self.SHIB_CONSUMER_URL
|
||||||
|
self.saml2plugin._send_service_provider_saml2_authn_response(
|
||||||
|
self.session)
|
||||||
|
token_json = self.saml2plugin.authenticated_response.json()['token']
|
||||||
|
token = self.saml2plugin.authenticated_response.headers[
|
||||||
|
'X-Subject-Token']
|
||||||
|
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'],
|
||||||
|
token_json)
|
||||||
|
|
||||||
|
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||||
|
token)
|
||||||
|
|
||||||
|
def test_consumer_url_mismatch_success(self):
|
||||||
|
self.saml2plugin._check_consumer_urls(
|
||||||
|
self.session, self.SHIB_CONSUMER_URL,
|
||||||
|
self.SHIB_CONSUMER_URL)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_consumer_url_mismatch(self):
|
||||||
|
self.simple_http('POST', self.SHIB_CONSUMER_URL)
|
||||||
|
invalid_consumer_url = uuid.uuid4().hex
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.ValidationError,
|
||||||
|
self.saml2plugin._check_consumer_urls,
|
||||||
|
self.session, self.SHIB_CONSUMER_URL,
|
||||||
|
invalid_consumer_url)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_custom_302_redirection(self):
|
||||||
|
self.simple_http('POST', self.SHIB_CONSUMER_URL,
|
||||||
|
body='BODY',
|
||||||
|
headers={'location': self.FEDERATION_AUTH_URL},
|
||||||
|
status=302)
|
||||||
|
self.simple_http(
|
||||||
|
'GET', self.FEDERATION_AUTH_URL,
|
||||||
|
body=jsonutils.dumps(saml2_fixtures.UNSCOPED_TOKEN),
|
||||||
|
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
|
||||||
|
self.session.redirect = False
|
||||||
|
response = self.session.post(
|
||||||
|
self.SHIB_CONSUMER_URL, data='CLIENT BODY')
|
||||||
|
self.assertEqual(302, response.status_code)
|
||||||
|
self.assertEqual(self.FEDERATION_AUTH_URL,
|
||||||
|
response.headers['location'])
|
||||||
|
|
||||||
|
response = self.saml2plugin._handle_http_302_ecp_redirect(
|
||||||
|
self.session, response, 'GET')
|
||||||
|
|
||||||
|
self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url)
|
||||||
|
self.assertEqual('GET', response.request.method)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_end_to_end_workflow(self):
|
||||||
|
self.simple_http('GET', self.FEDERATION_AUTH_URL,
|
||||||
|
body=self.make_oneline(
|
||||||
|
saml2_fixtures.SP_SOAP_RESPONSE))
|
||||||
|
self.simple_http('POST', self.IDENTITY_PROVIDER_URL,
|
||||||
|
body=saml2_fixtures.SAML2_ASSERTION)
|
||||||
|
self.simple_http(
|
||||||
|
'POST', self.SHIB_CONSUMER_URL,
|
||||||
|
body=jsonutils.dumps(saml2_fixtures.UNSCOPED_TOKEN),
|
||||||
|
content_type='application/json',
|
||||||
|
status=200,
|
||||||
|
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
|
||||||
|
|
||||||
|
self.session.redirect = False
|
||||||
|
response = self.saml2plugin.get_auth_ref(self.session)
|
||||||
|
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||||
|
response.auth_token)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
argparse
|
argparse
|
||||||
Babel>=1.3
|
Babel>=1.3
|
||||||
iso8601>=0.1.9
|
iso8601>=0.1.9
|
||||||
|
lxml>=2.3
|
||||||
netaddr>=0.7.6
|
netaddr>=0.7.6
|
||||||
oslo.config>=1.2.1
|
oslo.config>=1.2.1
|
||||||
pbr>=0.6,!=0.7,<1.0
|
pbr>=0.6,!=0.7,<1.0
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ keystoneclient.auth.plugin =
|
|||||||
v2token = keystoneclient.auth.identity.v2:Token
|
v2token = keystoneclient.auth.identity.v2:Token
|
||||||
v3password = keystoneclient.auth.identity.v3:Password
|
v3password = keystoneclient.auth.identity.v3:Password
|
||||||
v3token = keystoneclient.auth.identity.v3:Token
|
v3token = keystoneclient.auth.identity.v3:Token
|
||||||
|
v3unscopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2UnscopedToken
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
|
|||||||
Reference in New Issue
Block a user