SAML2 authentication plugins in keystoneauth
Move SAML2 related auth plugins directly to keystoneauth. Since SAML2 plugins requires ``lxml` which is a heavy dependency, plugins will be installed on request: $ pip install keystoneauth[saml2] Authentication plugins has been renamed to Saml2Password and ADFSPassword. Change-Id: I7872f7524902e4b723ab685c684e16162a4af781 Implements: bp saml2-to-ksa
This commit is contained in:
parent
bb98d0182d
commit
34993d332c
19
keystoneauth1/extras/_saml2/__init__.py
Normal file
19
keystoneauth1/extras/_saml2/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 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.extras._saml2 import v3
|
||||
|
||||
V3Saml2Password = v3.Saml2Password
|
||||
V3ADFSPassword = v3.ADFSPassword
|
||||
|
||||
|
||||
__all__ = ('V3Saml2Password', 'V3ADFSPassword')
|
53
keystoneauth1/extras/_saml2/_loading.py
Normal file
53
keystoneauth1/extras/_saml2/_loading.py
Normal file
@ -0,0 +1,53 @@
|
||||
# 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.extras import _saml2
|
||||
from keystoneauth1 import loading
|
||||
|
||||
|
||||
class Saml2Password(loading.BaseFederationLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return _saml2.V3Saml2Password
|
||||
|
||||
def get_options(self):
|
||||
options = super(Saml2Password, self).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.Opt('identity-provider-url',
|
||||
help=('An Identity Provider URL, where the SAML2 '
|
||||
'authentication request will be sent.')),
|
||||
loading.Opt('username', help='Username'),
|
||||
loading.Opt('password', help='Password')
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class ADFSPassword(loading.BaseFederationLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return _saml2.V3ADFSPassword
|
||||
|
||||
def get_options(self):
|
||||
options = super(ADFSPassword, self).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.Opt('service-provider-endpoint',
|
||||
help="Service Provider's Endpoint"),
|
||||
loading.Opt('username', help='Username'),
|
||||
loading.Opt('password', help='Password')
|
||||
])
|
||||
|
||||
return options
|
831
keystoneauth1/extras/_saml2/v3/__init__.py
Normal file
831
keystoneauth1/extras/_saml2/v3/__init__.py
Normal file
@ -0,0 +1,831 @@
|
||||
# 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 datetime
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
from six.moves import urllib
|
||||
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.identity import v3
|
||||
from keystoneauth1.identity.v3 import federation
|
||||
|
||||
__all__ = ('Saml2Password', 'ADFSPassword')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _BaseSAMLPlugin(federation.FederationBaseAuth):
|
||||
|
||||
HTTP_MOVED_TEMPORARILY = 302
|
||||
HTTP_SEE_OTHER = 303
|
||||
|
||||
def __init__(self, auth_url,
|
||||
identity_provider, identity_provider_url,
|
||||
username, password, protocol,
|
||||
**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
|
||||
|
||||
:param protocol: Protocol to be used for the authentication.
|
||||
The name must be equal to one configured at the
|
||||
keystone sp side. This value is used for building
|
||||
dynamic authentication URL.
|
||||
Typical value would be: saml2
|
||||
:type protocol: string
|
||||
|
||||
"""
|
||||
super(_BaseSAMLPlugin, self).__init__(
|
||||
auth_url=auth_url, identity_provider=identity_provider,
|
||||
protocol=protocol,
|
||||
**kwargs)
|
||||
self.identity_provider_url = identity_provider_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
@staticmethod
|
||||
def _first(_list):
|
||||
if len(_list) != 1:
|
||||
raise IndexError('Only single element list is acceptable')
|
||||
return _list[0]
|
||||
|
||||
@staticmethod
|
||||
def str_to_xml(content, msg=None, include_exc=True):
|
||||
try:
|
||||
return etree.XML(content)
|
||||
except etree.XMLSyntaxError as e:
|
||||
if not msg:
|
||||
msg = str(e)
|
||||
else:
|
||||
msg = msg % e if include_exc else msg
|
||||
raise exceptions.AuthorizationFailure(msg)
|
||||
|
||||
@staticmethod
|
||||
def xml_to_str(content, **kwargs):
|
||||
return etree.tostring(content, **kwargs)
|
||||
|
||||
|
||||
class Saml2AuthMethod(v3.AuthMethod):
|
||||
_method_parameters = []
|
||||
|
||||
def get_auth_data(self, session, auth, headers, **kwargs):
|
||||
raise exceptions.MethodNotImplemented('This method should never '
|
||||
'be called')
|
||||
|
||||
|
||||
class Saml2Password(_BaseSAMLPlugin):
|
||||
"""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>`_ on ECP.
|
||||
|
||||
Reference the `SAML2 ECP specification <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.
|
||||
|
||||
: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
|
||||
|
||||
|
||||
:param protocol: Protocol to be used for the authentication.
|
||||
The name must be equal to one configured at the
|
||||
keystone sp side. This value is used for building
|
||||
dynamic authentication URL.
|
||||
Typical value would be: saml2
|
||||
:type protocol: string
|
||||
|
||||
"""
|
||||
|
||||
_auth_method_class = Saml2AuthMethod
|
||||
|
||||
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 _handle_http_ecp_redirect(self, session, response, method, **kwargs):
|
||||
if response.status_code not in (self.HTTP_MOVED_TEMPORARILY,
|
||||
self.HTTP_SEE_OTHER):
|
||||
return response
|
||||
|
||||
location = response.headers['location']
|
||||
return session.request(location, method, authenticated=False,
|
||||
**kwargs)
|
||||
|
||||
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: keystoneauth1.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.federated_token_url,
|
||||
'sp_consumer_url': sp_response_consumer_url,
|
||||
'identity_provider': self.identity_provider,
|
||||
'idp_consumer_url': idp_sp_response_consumer_url
|
||||
}
|
||||
|
||||
raise exceptions.AuthorizationFailure(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: keystoneauth1.session.Session
|
||||
|
||||
"""
|
||||
sp_response = session.get(self.federated_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),
|
||||
authenticated=False, log=False)
|
||||
|
||||
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/303 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_ecp_redirect(
|
||||
session, response, method='GET',
|
||||
headers=self.ECP_SP_SAML2_REQUEST_HEADERS)
|
||||
|
||||
self.authenticated_response = response
|
||||
|
||||
def get_unscoped_auth_ref(self, session):
|
||||
"""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).
|
||||
|
||||
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
|
||||
``Saml2Password_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
|
||||
``Saml2Password_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
|
||||
``Saml2Password_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: keystoneauth1.session.Session
|
||||
|
||||
:returns: AccessInfo
|
||||
:rtype: :py:class:`keystoneauth1.access.AccessInfo`
|
||||
"""
|
||||
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 access.create(resp=self.authenticated_response)
|
||||
|
||||
|
||||
class ADFSPassword(_BaseSAMLPlugin):
|
||||
"""Authentication plugin for Microsoft ADFS2.0 IdPs."""
|
||||
|
||||
_auth_method_class = Saml2AuthMethod
|
||||
|
||||
DEFAULT_ADFS_TOKEN_EXPIRATION = 120
|
||||
|
||||
HEADER_SOAP = {"Content-Type": "application/soap+xml; charset=utf-8"}
|
||||
HEADER_X_FORM = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
|
||||
NAMESPACES = {
|
||||
's': 'http://www.w3.org/2003/05/soap-envelope',
|
||||
'a': 'http://www.w3.org/2005/08/addressing',
|
||||
'u': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-utility-1.0.xsd')
|
||||
}
|
||||
|
||||
ADFS_TOKEN_NAMESPACES = {
|
||||
's': 'http://www.w3.org/2003/05/soap-envelope',
|
||||
't': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512'
|
||||
}
|
||||
ADFS_ASSERTION_XPATH = ('/s:Envelope/s:Body'
|
||||
'/t:RequestSecurityTokenResponseCollection'
|
||||
'/t:RequestSecurityTokenResponse')
|
||||
|
||||
def __init__(self, auth_url, identity_provider, identity_provider_url,
|
||||
service_provider_endpoint, username, password,
|
||||
protocol, **kwargs):
|
||||
"""Constructor for ``ADFSPassword``.
|
||||
|
||||
: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
|
||||
authentication request will be sent.
|
||||
:type identity_provider_url: string
|
||||
|
||||
:param service_provider_endpoint: Endpoint where an assertion is being
|
||||
sent, for instance: ``https://host.domain/Shibboleth.sso/ADFS``
|
||||
:type service_provider_endpoint: string
|
||||
|
||||
:param username: User's login
|
||||
:type username: string
|
||||
|
||||
:param password: User's password
|
||||
:type password: string
|
||||
|
||||
"""
|
||||
|
||||
super(ADFSPassword, self).__init__(
|
||||
auth_url=auth_url, identity_provider=identity_provider,
|
||||
identity_provider_url=identity_provider_url,
|
||||
username=username, password=password, protocol=protocol)
|
||||
|
||||
self.service_provider_endpoint = service_provider_endpoint
|
||||
|
||||
def _cookies(self, session):
|
||||
"""Check if cookie jar is not empty.
|
||||
|
||||
keystoneauth1.session.Session object doesn't have a cookies attribute.
|
||||
We should then try fetching cookies from the underlying
|
||||
requests.Session object. If that fails too, there is something wrong
|
||||
and let Python raise the AttributeError.
|
||||
|
||||
:param session
|
||||
:returns: True if cookie jar is nonempty, False otherwise
|
||||
:raises AttributeError: in case cookies are not find anywhere
|
||||
|
||||
"""
|
||||
try:
|
||||
return bool(session.cookies)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return bool(session.session.cookies)
|
||||
|
||||
def _token_dates(self, fmt='%Y-%m-%dT%H:%M:%S.%fZ'):
|
||||
"""Calculate created and expires datetime objects.
|
||||
|
||||
The method is going to be used for building ADFS Request Security
|
||||
Token message. Time interval between ``created`` and ``expires``
|
||||
dates is now static and equals to 120 seconds. ADFS security tokens
|
||||
should not be live too long, as currently ``keystoneauth1``
|
||||
doesn't have mechanisms for reusing such tokens (every time ADFS authn
|
||||
method is called, keystoneauth1 will login with the ADFS instance).
|
||||
|
||||
:param fmt: Datetime format for specifying string format of a date.
|
||||
It should not be changed if the method is going to be used
|
||||
for building the ADFS security token request.
|
||||
:type fmt: string
|
||||
|
||||
"""
|
||||
|
||||
date_created = datetime.datetime.utcnow()
|
||||
date_expires = date_created + datetime.timedelta(
|
||||
seconds=self.DEFAULT_ADFS_TOKEN_EXPIRATION)
|
||||
return [_time.strftime(fmt) for _time in (date_created, date_expires)]
|
||||
|
||||
def _prepare_adfs_request(self):
|
||||
"""Build the ADFS Request Security Token SOAP message.
|
||||
|
||||
Some values like username or password are inserted in the request.
|
||||
|
||||
"""
|
||||
|
||||
WSS_SECURITY_NAMESPACE = {
|
||||
'o': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-secext-1.0.xsd')
|
||||
}
|
||||
|
||||
TRUST_NAMESPACE = {
|
||||
'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512'
|
||||
}
|
||||
|
||||
WSP_NAMESPACE = {
|
||||
'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy'
|
||||
}
|
||||
|
||||
WSA_NAMESPACE = {
|
||||
'wsa': 'http://www.w3.org/2005/08/addressing'
|
||||
}
|
||||
|
||||
root = etree.Element(
|
||||
'{http://www.w3.org/2003/05/soap-envelope}Envelope',
|
||||
nsmap=self.NAMESPACES)
|
||||
|
||||
header = etree.SubElement(
|
||||
root, '{http://www.w3.org/2003/05/soap-envelope}Header')
|
||||
action = etree.SubElement(
|
||||
header, "{http://www.w3.org/2005/08/addressing}Action")
|
||||
action.set(
|
||||
"{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1")
|
||||
action.text = ('http://docs.oasis-open.org/ws-sx/ws-trust/200512'
|
||||
'/RST/Issue')
|
||||
|
||||
messageID = etree.SubElement(
|
||||
header, '{http://www.w3.org/2005/08/addressing}MessageID')
|
||||
messageID.text = 'urn:uuid:' + uuid.uuid4().hex
|
||||
replyID = etree.SubElement(
|
||||
header, '{http://www.w3.org/2005/08/addressing}ReplyTo')
|
||||
address = etree.SubElement(
|
||||
replyID, '{http://www.w3.org/2005/08/addressing}Address')
|
||||
address.text = 'http://www.w3.org/2005/08/addressing/anonymous'
|
||||
|
||||
to = etree.SubElement(
|
||||
header, '{http://www.w3.org/2005/08/addressing}To')
|
||||
to.set("{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1")
|
||||
|
||||
security = etree.SubElement(
|
||||
header, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-secext-1.0.xsd}Security',
|
||||
nsmap=WSS_SECURITY_NAMESPACE)
|
||||
|
||||
security.set(
|
||||
"{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1")
|
||||
|
||||
timestamp = etree.SubElement(
|
||||
security, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-utility-1.0.xsd}Timestamp'))
|
||||
timestamp.set(
|
||||
('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-utility-1.0.xsd}Id'), '_0')
|
||||
|
||||
created = etree.SubElement(
|
||||
timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-utility-1.0.xsd}Created'))
|
||||
|
||||
expires = etree.SubElement(
|
||||
timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-utility-1.0.xsd}Expires'))
|
||||
|
||||
created.text, expires.text = self._token_dates()
|
||||
|
||||
usernametoken = etree.SubElement(
|
||||
security, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-'
|
||||
'wss-wssecurity-secext-1.0.xsd}UsernameToken')
|
||||
usernametoken.set(
|
||||
('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-'
|
||||
'wssecurity-utility-1.0.xsd}u'), "uuid-%s-1" % uuid.uuid4().hex)
|
||||
|
||||
username = etree.SubElement(
|
||||
usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-'
|
||||
'200401-wss-wssecurity-secext-1.0.xsd}Username'))
|
||||
password = etree.SubElement(
|
||||
usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-'
|
||||
'200401-wss-wssecurity-secext-1.0.xsd}Password'),
|
||||
Type=('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-'
|
||||
'username-token-profile-1.0#PasswordText'))
|
||||
|
||||
body = etree.SubElement(
|
||||
root, "{http://www.w3.org/2003/05/soap-envelope}Body")
|
||||
|
||||
request_security_token = etree.SubElement(
|
||||
body, ('{http://docs.oasis-open.org/ws-sx/ws-trust/200512}'
|
||||
'RequestSecurityToken'), nsmap=TRUST_NAMESPACE)
|
||||
|
||||
applies_to = etree.SubElement(
|
||||
request_security_token,
|
||||
'{http://schemas.xmlsoap.org/ws/2004/09/policy}AppliesTo',
|
||||
nsmap=WSP_NAMESPACE)
|
||||
|
||||
endpoint_reference = etree.SubElement(
|
||||
applies_to,
|
||||
'{http://www.w3.org/2005/08/addressing}EndpointReference',
|
||||
nsmap=WSA_NAMESPACE)
|
||||
|
||||
wsa_address = etree.SubElement(
|
||||
endpoint_reference,
|
||||
'{http://www.w3.org/2005/08/addressing}Address')
|
||||
|
||||
keytype = etree.SubElement(
|
||||
request_security_token,
|
||||
'{http://docs.oasis-open.org/ws-sx/ws-trust/200512}KeyType')
|
||||
keytype.text = ('http://docs.oasis-open.org/ws-sx/'
|
||||
'ws-trust/200512/Bearer')
|
||||
|
||||
request_type = etree.SubElement(
|
||||
request_security_token,
|
||||
'{http://docs.oasis-open.org/ws-sx/ws-trust/200512}RequestType')
|
||||
request_type.text = ('http://docs.oasis-open.org/ws-sx/'
|
||||
'ws-trust/200512/Issue')
|
||||
token_type = etree.SubElement(
|
||||
request_security_token,
|
||||
'{http://docs.oasis-open.org/ws-sx/ws-trust/200512}TokenType')
|
||||
token_type.text = 'urn:oasis:names:tc:SAML:1.0:assertion'
|
||||
|
||||
# After constructing the request, let's plug in some values
|
||||
username.text = self.username
|
||||
password.text = self.password
|
||||
to.text = self.identity_provider_url
|
||||
wsa_address.text = self.service_provider_endpoint
|
||||
|
||||
self.prepared_request = root
|
||||
|
||||
def _get_adfs_security_token(self, session):
|
||||
"""Send ADFS Security token to the ADFS server.
|
||||
|
||||
Store the result in the instance attribute and raise an exception in
|
||||
case the response is not valid XML data.
|
||||
|
||||
If a user cannot authenticate due to providing bad credentials, the
|
||||
ADFS2.0 server will return a HTTP 500 response and a XML Fault message.
|
||||
If ``exceptions.InternalServerError`` is caught, the method tries to
|
||||
parse the XML response.
|
||||
If parsing is unsuccessful, an ``exceptions.AuthorizationFailure`` is
|
||||
raised with a reason from the XML fault. Otherwise an original
|
||||
``exceptions.InternalServerError`` is re-raised.
|
||||
|
||||
:param session : a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:raises keystoneauth1.exceptions.AuthorizationFailure: when HTTP
|
||||
response from the ADFS server is not a valid XML ADFS security
|
||||
token.
|
||||
:raises keystoneauth1.exceptions.InternalServerError: If response
|
||||
status code is HTTP 500 and the response XML cannot be
|
||||
recognized.
|
||||
|
||||
"""
|
||||
def _get_failure(e):
|
||||
xpath = '/s:Envelope/s:Body/s:Fault/s:Code/s:Subcode/s:Value'
|
||||
content = e.response.content
|
||||
try:
|
||||
obj = self.str_to_xml(content).xpath(
|
||||
xpath, namespaces=self.NAMESPACES)
|
||||
obj = self._first(obj)
|
||||
return obj.text
|
||||
# NOTE(marek-denis): etree.Element.xpath() doesn't raise an
|
||||
# exception, it just returns an empty list. In that case, _first()
|
||||
# will raise IndexError and we should treat it as an indication XML
|
||||
# is not valid. exceptions.AuthorizationFailure can be raised from
|
||||
# str_to_xml(), however since server returned HTTP 500 we should
|
||||
# re-raise exceptions.InternalServerError.
|
||||
except (IndexError, exceptions.AuthorizationFailure):
|
||||
raise e
|
||||
|
||||
request_security_token = self.xml_to_str(self.prepared_request)
|
||||
try:
|
||||
response = session.post(
|
||||
url=self.identity_provider_url, headers=self.HEADER_SOAP,
|
||||
data=request_security_token, authenticated=False)
|
||||
except exceptions.InternalServerError as e:
|
||||
reason = _get_failure(e)
|
||||
raise exceptions.AuthorizationFailure(reason)
|
||||
msg = ('Error parsing XML returned from '
|
||||
'the ADFS Identity Provider, reason: %s')
|
||||
self.adfs_token = self.str_to_xml(response.content, msg)
|
||||
|
||||
def _prepare_sp_request(self):
|
||||
"""Prepare ADFS Security Token to be sent to the Service Provider.
|
||||
|
||||
The method works as follows:
|
||||
* Extract SAML2 assertion from the ADFS Security Token.
|
||||
* Replace namespaces
|
||||
* urlencode assertion
|
||||
* concatenate static string with the encoded assertion
|
||||
|
||||
"""
|
||||
assertion = self.adfs_token.xpath(
|
||||
self.ADFS_ASSERTION_XPATH, namespaces=self.ADFS_TOKEN_NAMESPACES)
|
||||
assertion = self._first(assertion)
|
||||
assertion = self.xml_to_str(assertion)
|
||||
# TODO(marek-denis): Ideally no string replacement should occur.
|
||||
# Unfortunately lxml doesn't allow for namespaces changing in-place and
|
||||
# probably the only solution good for now is to build the assertion
|
||||
# from scratch and reuse values from the adfs security token.
|
||||
assertion = assertion.replace(
|
||||
b'http://docs.oasis-open.org/ws-sx/ws-trust/200512',
|
||||
b'http://schemas.xmlsoap.org/ws/2005/02/trust')
|
||||
|
||||
encoded_assertion = urllib.parse.quote(assertion)
|
||||
self.encoded_assertion = 'wa=wsignin1.0&wresult=' + encoded_assertion
|
||||
|
||||
def _send_assertion_to_service_provider(self, session):
|
||||
"""Send prepared assertion to a service provider.
|
||||
|
||||
As the assertion doesn't contain a protected resource, the value from
|
||||
the ``location`` header is not valid and we should not let the Session
|
||||
object get redirected there. The aim of this call is to get a cookie in
|
||||
the response which is required for entering a protected endpoint.
|
||||
|
||||
:param session : a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:raises: Corresponding HTTP error exception
|
||||
|
||||
"""
|
||||
session.post(
|
||||
url=self.service_provider_endpoint, data=self.encoded_assertion,
|
||||
headers=self.HEADER_X_FORM, redirect=False, authenticated=False)
|
||||
|
||||
def _access_service_provider(self, session):
|
||||
"""Access protected endpoint and fetch unscoped token.
|
||||
|
||||
After federated authentication workflow a protected endpoint should be
|
||||
accessible with the session object. The access is granted basing on the
|
||||
cookies stored within the session object. If, for some reason no
|
||||
cookies are present (quantity test) it means something went wrong and
|
||||
user will not be able to fetch an unscoped token. In that case an
|
||||
``exceptions.AuthorizationFailure` exception is raised and no HTTP call
|
||||
is even made.
|
||||
|
||||
:param session : a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:raises keystoneauth1.exceptions.AuthorizationFailure: in case session
|
||||
object has empty cookie jar.
|
||||
|
||||
"""
|
||||
if self._cookies(session) is False:
|
||||
raise exceptions.AuthorizationFailure(
|
||||
"Session object doesn't contain a cookie, therefore you are "
|
||||
"not allowed to enter the Identity Provider's protected area.")
|
||||
self.authenticated_response = session.get(self.federated_token_url,
|
||||
authenticated=False)
|
||||
|
||||
def get_unscoped_auth_ref(self, session, *kwargs):
|
||||
"""Retrieve unscoped token after authentcation with ADFS server.
|
||||
|
||||
This is a multistep process:
|
||||
|
||||
* Prepare ADFS Request Securty Token -
|
||||
build a etree.XML object filling certain attributes with proper user
|
||||
credentials, created/expires dates (ticket is be valid for 120
|
||||
seconds as currently we don't handle reusing ADFS issued security
|
||||
tokens).
|
||||
|
||||
* Send ADFS Security token to the ADFS server. Step handled by
|
||||
|
||||
* Receive and parse security token, extract actual SAML assertion and
|
||||
prepare a request addressed for the Service Provider endpoint.
|
||||
This also includes changing namespaces in the XML document. Step
|
||||
handled by ``ADFSPassword._prepare_sp_request()`` method.
|
||||
|
||||
* Send prepared assertion to the Service Provider endpoint. Usually
|
||||
the server will respond with HTTP 301 code which should be ignored as
|
||||
the 'location' header doesn't contain protected area. The goal of
|
||||
this operation is fetching the session cookie which later allows for
|
||||
accessing protected URL endpoints. Step handed by
|
||||
``ADFSPassword._send_assertion_to_service_provider()`` method.
|
||||
|
||||
* Once the session cookie is issued, the protected endpoint can be
|
||||
accessed and an unscoped token can be retrieved. Step handled by
|
||||
``ADFSPassword._access_service_provider()`` method.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: AccessInfo
|
||||
:rtype: :py:class:`keystoneauth1.access.AccessInfo`
|
||||
|
||||
"""
|
||||
self._prepare_adfs_request()
|
||||
self._get_adfs_security_token(session)
|
||||
self._prepare_sp_request()
|
||||
self._send_assertion_to_service_provider(session)
|
||||
self._access_service_provider(session)
|
||||
|
||||
return access.create(resp=self.authenticated_response)
|
116
keystoneauth1/tests/unit/client_fixtures.py
Normal file
116
keystoneauth1/tests/unit/client_fixtures.py
Normal file
@ -0,0 +1,116 @@
|
||||
# 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 import fixture
|
||||
|
||||
|
||||
def project_scoped_token():
|
||||
f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a',
|
||||
user_name='exampleuser',
|
||||
user_domain_id='4e6893b7ba0b4006840c3845660b86ed',
|
||||
user_domain_name='exampledomain',
|
||||
expires='2010-11-01T03:32:15-05:00',
|
||||
project_id='225da22d3ce34b15877ea70b2a575f58',
|
||||
project_name='exampleproject',
|
||||
project_domain_id='4e6893b7ba0b4006840c3845660b86ed',
|
||||
project_domain_name='exampledomain')
|
||||
|
||||
f.add_role(id='76e72a', name='admin')
|
||||
f.add_role(id='f4f392', name='member')
|
||||
|
||||
region = 'RegionOne'
|
||||
tenant = '225da22d3ce34b15877ea70b2a575f58'
|
||||
|
||||
s = f.add_service('volume')
|
||||
s.add_standard_endpoints(public='http://public.com:8776/v1/%s' % tenant,
|
||||
internal='http://internal:8776/v1/%s' % tenant,
|
||||
admin='http://admin:8776/v1/%s' % tenant,
|
||||
region=region)
|
||||
|
||||
s = f.add_service('image')
|
||||
s.add_standard_endpoints(public='http://public.com:9292/v1',
|
||||
internal='http://internal:9292/v1',
|
||||
admin='http://admin:9292/v1',
|
||||
region=region)
|
||||
|
||||
s = f.add_service('compute')
|
||||
s.add_standard_endpoints(public='http://public.com:8774/v2/%s' % tenant,
|
||||
internal='http://internal:8774/v2/%s' % tenant,
|
||||
admin='http://admin:8774/v2/%s' % tenant,
|
||||
region=region)
|
||||
|
||||
s = f.add_service('ec2')
|
||||
s.add_standard_endpoints(public='http://public.com:8773/services/Cloud',
|
||||
internal='http://internal:8773/services/Cloud',
|
||||
admin='http://admin:8773/services/Admin',
|
||||
region=region)
|
||||
|
||||
s = f.add_service('identity')
|
||||
s.add_standard_endpoints(public='http://public.com:5000/v3',
|
||||
internal='http://internal:5000/v3',
|
||||
admin='http://admin:35357/v3',
|
||||
region=region)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def domain_scoped_token():
|
||||
f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a',
|
||||
user_name='exampleuser',
|
||||
user_domain_id='4e6893b7ba0b4006840c3845660b86ed',
|
||||
user_domain_name='exampledomain',
|
||||
expires='2010-11-01T03:32:15-05:00',
|
||||
domain_id='8e9283b7ba0b1038840c3842058b86ab',
|
||||
domain_name='anotherdomain')
|
||||
|
||||
f.add_role(id='76e72a', name='admin')
|
||||
f.add_role(id='f4f392', name='member')
|
||||
region = 'RegionOne'
|
||||
|
||||
s = f.add_service('volume')
|
||||
s.add_standard_endpoints(public='http://public.com:8776/v1/None',
|
||||
internal='http://internal.com:8776/v1/None',
|
||||
admin='http://admin.com:8776/v1/None',
|
||||
region=region)
|
||||
|
||||
s = f.add_service('image')
|
||||
s.add_standard_endpoints(public='http://public.com:9292/v1',
|
||||
internal='http://internal:9292/v1',
|
||||
admin='http://admin:9292/v1',
|
||||
region=region)
|
||||
|
||||
s = f.add_service('compute')
|
||||
s.add_standard_endpoints(public='http://public.com:8774/v1.1/None',
|
||||
internal='http://internal:8774/v1.1/None',
|
||||
admin='http://admin:8774/v1.1/None',
|
||||
region=region)
|
||||
|
||||
s = f.add_service('ec2')
|
||||
s.add_standard_endpoints(public='http://public.com:8773/services/Cloud',
|
||||
internal='http://internal:8773/services/Cloud',
|
||||
admin='http://admin:8773/services/Admin',
|
||||
region=region)
|
||||
|
||||
s = f.add_service('identity')
|
||||
s.add_standard_endpoints(public='http://public.com:5000/v3',
|
||||
internal='http://internal:5000/v3',
|
||||
admin='http://admin:35357/v3',
|
||||
region=region)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed'
|
||||
|
||||
AUTH_RESPONSE_HEADERS = {
|
||||
'X-Subject-Token': AUTH_SUBJECT_TOKEN,
|
||||
}
|
0
keystoneauth1/tests/unit/extras/__init__.py
Normal file
0
keystoneauth1/tests/unit/extras/__init__.py
Normal file
0
keystoneauth1/tests/unit/extras/saml2/__init__.py
Normal file
0
keystoneauth1/tests/unit/extras/saml2/__init__.py
Normal file
@ -0,0 +1,132 @@
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal</a:Action>
|
||||
<a:RelatesTo>urn:uuid:487c064b-b7c6-4654-b4d4-715f9961170e</a:RelatesTo>
|
||||
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
|
||||
<u:Timestamp u:Id="_0">
|
||||
<u:Created>2014-08-05T18:36:14.235Z</u:Created>
|
||||
<u:Expires>2014-08-05T18:41:14.235Z</u:Expires>
|
||||
</u:Timestamp>
|
||||
</o:Security>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
|
||||
<trust:RequestSecurityTokenResponse>
|
||||
<trust:Lifetime>
|
||||
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-08-05T18:36:14.063Z</wsu:Created>
|
||||
<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-08-05T19:36:14.063Z</wsu:Expires>
|
||||
</trust:Lifetime>
|
||||
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
|
||||
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
|
||||
<wsa:Address>https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS</wsa:Address>
|
||||
</wsa:EndpointReference>
|
||||
</wsp:AppliesTo>
|
||||
<trust:RequestedSecurityToken>
|
||||
<saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f" Issuer="https://cern.ch/login" IssueInstant="2014-08-05T18:36:14.235Z" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
|
||||
<saml:Conditions NotBefore="2014-08-05T18:36:14.063Z" NotOnOrAfter="2014-08-05T19:36:14.063Z">
|
||||
<saml:AudienceRestrictionCondition>
|
||||
<saml:Audience>https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS</saml:Audience>
|
||||
</saml:AudienceRestrictionCondition>
|
||||
</saml:Conditions>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Subject>
|
||||
<saml:NameIdentifier Format="http://schemas.xmlsoap.org/claims/UPN">marek.denis@cern.ch</saml:NameIdentifier>
|
||||
<saml:SubjectConfirmation>
|
||||
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
<saml:Attribute AttributeName="UPN" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>marek.denis@cern.ch</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="EmailAddress" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>marek.denis@cern.ch</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="CommonName" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>madenis</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="role" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
|
||||
<saml:AttributeValue>CERN Users</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="Group" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>Domain Users</saml:AttributeValue>
|
||||
<saml:AttributeValue>occupants-bldg-31</saml:AttributeValue>
|
||||
<saml:AttributeValue>CERN-Direct-Employees</saml:AttributeValue>
|
||||
<saml:AttributeValue>ca-dev-allowed</saml:AttributeValue>
|
||||
<saml:AttributeValue>cernts-cerntstest-users</saml:AttributeValue>
|
||||
<saml:AttributeValue>staf-fell-pjas-at-cern</saml:AttributeValue>
|
||||
<saml:AttributeValue>ELG-CERN</saml:AttributeValue>
|
||||
<saml:AttributeValue>student-club-new-members</saml:AttributeValue>
|
||||
<saml:AttributeValue>pawel-dynamic-test-82</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="DisplayName" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>Marek Kamil Denis</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="MobileNumber" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>+5555555</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="Building" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>31S-013</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="Firstname" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>Marek Kamil</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="Lastname" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>Denis</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="IdentityClass" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>CERN Registered</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="Federation" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>CERN</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute AttributeName="AuthLevel" AttributeNamespace="http://schemas.xmlsoap.org/claims">
|
||||
<saml:AttributeValue>Normal</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
<saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password" AuthenticationInstant="2014-08-05T18:36:14.032Z">
|
||||
<saml:Subject>
|
||||
<saml:NameIdentifier Format="http://schemas.xmlsoap.org/claims/UPN">marek.denis@cern.ch</saml:NameIdentifier>
|
||||
<saml:SubjectConfirmation>
|
||||
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
</saml:AuthenticationStatement>
|
||||
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
|
||||
<SignedInfo>
|
||||
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
|
||||
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
|
||||
<Reference URI="#_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f">
|
||||
<Transforms>
|
||||
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
|
||||
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
|
||||
</Transforms>
|
||||
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
|
||||
<DigestValue>EaZ/2d0KAY5un9akV3++Npyk6hBc8JuTYs2S3lSxUeQ=</DigestValue>
|
||||
</Reference>
|
||||
</SignedInfo>
|
||||
<SignatureValue>CxYiYvNsbedhHdmDbb9YQCBy6Ppus3bNJdw2g2HLq0VU2yRhv23mUW05I89Hs4yG4OcCo0uOZ3zaeNFbSNXMW+Mr996tAXtujKjgyrCXNJAToE+gwltvGxwY1EluSbe3IzoSM3Ao87mKhxGOSzlDhuN7dQ9Rv6l/J4gUjbOO5SIX4pdZ6mVF7cHEfe9x+H8Lg15YjnElQUEaPi+NSW5jYTdtIpsB4ORxJvALuSt6+4doDYc9wuwBiWkEdnBHAQBINoKpAV2oy0/C85SBX3IdRhxUznmL5yEUmf8JvPccXecMPqJow0L43mnCdu74xPwU0as3MNfYQ10kLvHXHfIExg==</SignatureValue>
|
||||
<KeyInfo>
|
||||
<X509Data>
|
||||
<X509Certificate>MIIIEjCCBfqgAwIBAgIKLYgjvQAAAAAAMDANBgkqhkiG9w0BAQsFADBRMRIwEAYKCZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxDRVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMTEwODA4Mzg1NVoXDTIzMDcyOTA5MTkzOFowVjESMBAGCgmSJomT8ixkARkWAmNoMRQwEgYKCZImiZPyLGQBGRYEY2VybjESMBAGA1UECxMJY29tcHV0ZXJzMRYwFAYDVQQDEw1sb2dpbi5jZXJuLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp6t1C0SGlLddL2M+ltffGioTnDT3eztOxlA9bAGuvB8/Rjym8en6+ET9boM02CyoR5Vpn8iElXVWccAExPIQEq70D6LPe86vb+tYhuKPeLfuICN9Z0SMQ4f+57vk61Co1/uw/8kPvXlyd+Ai8Dsn/G0hpH67bBI9VOQKfpJqclcSJuSlUB5PJffvMUpr29B0eRx8LKFnIHbDILSu6nVbFLcadtWIjbYvoKorXg3J6urtkz+zEDeYMTvA6ZGOFf/Xy5eGtroSq9csSC976tx+umKEPhXBA9AcpiCV9Cj5axN03Aaa+iTE36jpnjcd9d02dy5Q9jE2nUN6KXnB6qF6eQIDAQABo4ID5TCCA+EwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIg73QCYLtjQ2G7Ysrgd71N4WA0GIehd2yb4Wu9TkCAWQCARkwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIFoDBoBgNVHSAEYTBfMF0GCisGAQQBYAoEAQEwTzBNBggrBgEFBQcCARZBaHR0cDovL2NhLWRvY3MuY2Vybi5jaC9jYS1kb2NzL2NwLWNwcy9jZXJuLXRydXN0ZWQtY2EyLWNwLWNwcy5wZGYwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDATAdBgNVHQ4EFgQUqtJcwUXasyM6sRaO5nCMFoFDenMwGAYDVR0RBBEwD4INbG9naW4uY2Vybi5jaDAfBgNVHSMEGDAWgBQdkBnqyM7MPI0UsUzZ7BTiYUADYTCCASoGA1UdHwSCASEwggEdMIIBGaCCARWgggERhkdodHRwOi8vY2FmaWxlcy5jZXJuLmNoL2NhZmlsZXMvY3JsL0NFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5LmNybIaBxWxkYXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049Q0VSTlBLSTA3LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWNlcm4sREM9Y2g/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIIBVAYIKwYBBQUHAQEEggFGMIIBQjBcBggrBgEFBQcwAoZQaHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NlcnRpZmljYXRlcy9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5jcnQwgbsGCCsGAQUFBzAChoGubGRhcDovLy9DTj1DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jZXJuLmNoL29jc3AwDQYJKoZIhvcNAQELBQADggIBAGKZ3bknTCfNuh4TMaL3PuvBFjU8LQ5NKY9GLZvY2ibYMRk5Is6eWRgyUsy1UJRQdaQQPnnysqrGq8VRw/NIFotBBsA978/+jj7v4e5Kr4o8HvwAQNLBxNmF6XkDytpLL701FcNEGRqIsoIhNzihi2VBADLC9HxljEyPT52IR767TMk/+xTOqClceq3sq6WRD4m+xaWRUJyOhn+Pqr+wbhXIw4wzHC6X0hcLj8P9Povtm6VmKkN9JPuymMo/0+zSrUt2+TYfmbbEKYJSP0+sceQ76IKxxmSdKAr1qDNE8v+c3DvPM2PKmfivwaV2l44FdP8ulzqTgphkYcN1daa9Oc+qJeyu/eL7xWzk6Zq5R+jVrMlM0p1y2XczI7Hoc96TMOcbVnwgMcVqRM9p57VItn6XubYPR0C33i1yUZjkWbIfqEjq6Vev6lVgngOyzu+hqC/8SDyORA3dlF9aZOD13kPZdF/JRphHREQtaRydAiYRlE/WHTvOcY52jujDftUR6oY0eWaWkwSHbX+kDFx8IlR8UtQCUgkGHBGwnOYLIGu7SRDGSfOBOiVhxKoHWVk/pL6eKY2SkmyOmmgO4JnQGg95qeAOMG/EQZt/2x8GAavUqGvYy9dPFwFf08678hQqkjNSuex7UD0ku8OP1QKvpP44l6vZhFc6A5XqjdU9lus1</X509Certificate>
|
||||
</X509Data>
|
||||
</KeyInfo>
|
||||
</Signature>
|
||||
</saml:Assertion>
|
||||
</trust:RequestedSecurityToken>
|
||||
<trust:RequestedAttachedReference>
|
||||
<o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
|
||||
<o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f</o:KeyIdentifier>
|
||||
</o:SecurityTokenReference>
|
||||
</trust:RequestedAttachedReference>
|
||||
<trust:RequestedUnattachedReference>
|
||||
<o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
|
||||
<o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f</o:KeyIdentifier>
|
||||
</o:SecurityTokenReference>
|
||||
</trust:RequestedUnattachedReference>
|
||||
<trust:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</trust:TokenType>
|
||||
<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
|
||||
<trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
|
||||
</trust:RequestSecurityTokenResponse>
|
||||
</trust:RequestSecurityTokenResponseCollection>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
@ -0,0 +1,19 @@
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://www.w3.org/2005/08/addressing/soap/fault</a:Action>
|
||||
<a:RelatesTo>urn:uuid:89c47849-2622-4cdc-bb06-1d46c89ed12d</a:RelatesTo>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<s:Fault>
|
||||
<s:Code>
|
||||
<s:Value>s:Sender</s:Value>
|
||||
<s:Subcode>
|
||||
<s:Value xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">a:FailedAuthentication</s:Value>
|
||||
</s:Subcode>
|
||||
</s:Code>
|
||||
<s:Reason>
|
||||
<s:Text xml:lang="en-US">At least one security token in the message could not be validated.</s:Text>
|
||||
</s:Reason>
|
||||
</s:Fault>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
171
keystoneauth1/tests/unit/extras/saml2/fixtures.py
Normal file
171
keystoneauth1/tests/unit/extras/saml2/fixtures.py
Normal file
@ -0,0 +1,171 @@
|
||||
# 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 six
|
||||
|
||||
SP_SOAP_RESPONSE = six.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 = six.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PROJECTS = {
|
||||
"projects": [
|
||||
{
|
||||
"domain_id": "37ef61",
|
||||
"enabled": 'true',
|
||||
"id": "12d706",
|
||||
"links": {
|
||||
"self": "http://identity:35357/v3/projects/12d706"
|
||||
},
|
||||
"name": "a project name"
|
||||
},
|
||||
{
|
||||
"domain_id": "37ef61",
|
||||
"enabled": 'true',
|
||||
"id": "9ca0eb",
|
||||
"links": {
|
||||
"self": "http://identity:35357/v3/projects/9ca0eb"
|
||||
},
|
||||
"name": "another project"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "http://identity:35357/v3/OS-FEDERATION/projects",
|
||||
"previous": 'null',
|
||||
"next": 'null'
|
||||
}
|
||||
}
|
||||
|
||||
DOMAINS = {
|
||||
"domains": [
|
||||
{
|
||||
"description": "desc of domain",
|
||||
"enabled": 'true',
|
||||
"id": "37ef61",
|
||||
"links": {
|
||||
"self": "http://identity:35357/v3/domains/37ef61"
|
||||
},
|
||||
"name": "my domain"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "http://identity:35357/v3/OS-FEDERATION/domains",
|
||||
"previous": 'null',
|
||||
"next": 'null'
|
||||
}
|
||||
}
|
520
keystoneauth1/tests/unit/extras/saml2/test_identity_v3_saml2.py
Normal file
520
keystoneauth1/tests/unit/extras/saml2/test_identity_v3_saml2.py
Normal file
@ -0,0 +1,520 @@
|
||||
# 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 os
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
from six.moves import urllib
|
||||
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.extras import _saml2 as saml2
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1.tests.unit import client_fixtures
|
||||
from keystoneauth1.tests.unit.extras.saml2 import fixtures as saml2_fixtures
|
||||
from keystoneauth1.tests.unit import utils
|
||||
|
||||
ROOTDIR = os.path.dirname(os.path.abspath(__file__))
|
||||
XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/')
|
||||
|
||||
|
||||
def make_oneline(s):
|
||||
return etree.tostring(etree.XML(s)).replace(b'\n', b'')
|
||||
|
||||
|
||||
def _load_xml(filename):
|
||||
with open(XMLDIR + filename, 'rb') as f:
|
||||
return make_oneline(f.read())
|
||||
|
||||
|
||||
class PluginMixin(object):
|
||||
|
||||
TEST_URL = 'https://keystone:5000/v3'
|
||||
session = session.Session()
|
||||
|
||||
|
||||
class AuthenticateviaSAML2Tests(utils.TestCase, PluginMixin):
|
||||
|
||||
GROUP = 'auth'
|
||||
TEST_TOKEN = uuid.uuid4().hex
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticateviaSAML2Tests, self).setUp()
|
||||
|
||||
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.V3Saml2Password(
|
||||
self.TEST_URL,
|
||||
self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL,
|
||||
self.TEST_USER, self.TEST_TOKEN, self.PROTOCOL)
|
||||
|
||||
def test_initial_sp_call(self):
|
||||
"""Test initial call, expect SOAP message."""
|
||||
self.requests_mock.get(
|
||||
self.FEDERATION_AUTH_URL,
|
||||
content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE))
|
||||
a = self.saml2plugin._send_service_provider_request(self.session)
|
||||
|
||||
self.assertFalse(a)
|
||||
|
||||
fixture_soap_response = make_oneline(
|
||||
saml2_fixtures.SP_SOAP_RESPONSE)
|
||||
|
||||
sp_soap_response = 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)))
|
||||
|
||||
def test_initial_sp_call_when_saml_authenticated(self):
|
||||
self.requests_mock.get(
|
||||
self.FEDERATION_AUTH_URL,
|
||||
json=saml2_fixtures.UNSCOPED_TOKEN,
|
||||
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
|
||||
|
||||
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'])
|
||||
|
||||
def test_get_unscoped_token_when_authenticated(self):
|
||||
self.requests_mock.get(
|
||||
self.FEDERATION_AUTH_URL,
|
||||
json=saml2_fixtures.UNSCOPED_TOKEN,
|
||||
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||
'Content-Type': 'application/json'})
|
||||
|
||||
token = self.saml2plugin.get_auth_ref(self.session)
|
||||
|
||||
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||
token.auth_token)
|
||||
|
||||
def test_initial_sp_call_invalid_response(self):
|
||||
"""Send initial SP HTTP request and receive wrong server response."""
|
||||
self.requests_mock.get(self.FEDERATION_AUTH_URL,
|
||||
text='NON XML RESPONSE')
|
||||
|
||||
self.assertRaises(
|
||||
exceptions.AuthorizationFailure,
|
||||
self.saml2plugin._send_service_provider_request,
|
||||
self.session)
|
||||
|
||||
def test_send_authn_req_to_idp(self):
|
||||
self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
|
||||
content=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 = make_oneline(etree.tostring(
|
||||
self.saml2plugin.saml2_idp_authn_response))
|
||||
|
||||
saml2_assertion_oneline = 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)
|
||||
|
||||
def test_fail_basicauth_idp_authentication(self):
|
||||
self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
|
||||
status_code=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.V3Saml2Password,
|
||||
self.TEST_URL, self.IDENTITY_PROVIDER,
|
||||
self.IDENTITY_PROVIDER_URL)
|
||||
|
||||
def test_send_authn_response_to_sp(self):
|
||||
self.requests_mock.post(
|
||||
self.SHIB_CONSUMER_URL,
|
||||
json=saml2_fixtures.UNSCOPED_TOKEN,
|
||||
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)
|
||||
|
||||
def test_consumer_url_mismatch(self):
|
||||
self.requests_mock.post(self.SHIB_CONSUMER_URL)
|
||||
invalid_consumer_url = uuid.uuid4().hex
|
||||
self.assertRaises(
|
||||
exceptions.AuthorizationFailure,
|
||||
self.saml2plugin._check_consumer_urls,
|
||||
self.session, self.SHIB_CONSUMER_URL,
|
||||
invalid_consumer_url)
|
||||
|
||||
def test_custom_302_redirection(self):
|
||||
self.requests_mock.post(
|
||||
self.SHIB_CONSUMER_URL,
|
||||
text='BODY',
|
||||
headers={'location': self.FEDERATION_AUTH_URL},
|
||||
status_code=302)
|
||||
|
||||
self.requests_mock.get(
|
||||
self.FEDERATION_AUTH_URL,
|
||||
json=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_ecp_redirect(
|
||||
self.session, response, 'GET')
|
||||
|
||||
self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url)
|
||||
self.assertEqual('GET', response.request.method)
|
||||
|
||||
def test_custom_303_redirection(self):
|
||||
self.requests_mock.post(
|
||||
self.SHIB_CONSUMER_URL,
|
||||
text='BODY',
|
||||
headers={'location': self.FEDERATION_AUTH_URL},
|
||||
status_code=303)
|
||||
|
||||
self.requests_mock.get(
|
||||
self.FEDERATION_AUTH_URL,
|
||||
json=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(303, response.status_code)
|
||||
self.assertEqual(self.FEDERATION_AUTH_URL,
|
||||
response.headers['location'])
|
||||
|
||||
response = self.saml2plugin._handle_http_ecp_redirect(
|
||||
self.session, response, 'GET')
|
||||
|
||||
self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url)
|
||||
self.assertEqual('GET', response.request.method)
|
||||
|
||||
def test_end_to_end_workflow(self):
|
||||
self.requests_mock.get(
|
||||
self.FEDERATION_AUTH_URL,
|
||||
content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE))
|
||||
|
||||
self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
|
||||
content=saml2_fixtures.SAML2_ASSERTION)
|
||||
|
||||
self.requests_mock.post(
|
||||
self.SHIB_CONSUMER_URL,
|
||||
json=saml2_fixtures.UNSCOPED_TOKEN,
|
||||
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||
'Content-Type': 'application/json'})
|
||||
|
||||
self.session.redirect = False
|
||||
response = self.saml2plugin.get_auth_ref(self.session)
|
||||
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER,
|
||||
response.auth_token)
|
||||
|
||||
|
||||
class AuthenticateviaADFSTests(utils.TestCase, PluginMixin):
|
||||
|
||||
GROUP = 'auth'
|
||||
|
||||
NAMESPACES = {
|
||||
's': 'http://www.w3.org/2003/05/soap-envelope',
|
||||
'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512',
|
||||
'wsa': 'http://www.w3.org/2005/08/addressing',
|
||||
'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy',
|
||||
'a': 'http://www.w3.org/2005/08/addressing',
|
||||
'o': ('http://docs.oasis-open.org/wss/2004/01/oasis'
|
||||
'-200401-wss-wssecurity-secext-1.0.xsd')
|
||||
}
|
||||
|
||||
USER_XPATH = ('/s:Envelope/s:Header'
|
||||
'/o:Security'
|
||||
'/o:UsernameToken'
|
||||
'/o:Username')
|
||||
PASSWORD_XPATH = ('/s:Envelope/s:Header'
|
||||
'/o:Security'
|
||||
'/o:UsernameToken'
|
||||
'/o:Password')
|
||||
ADDRESS_XPATH = ('/s:Envelope/s:Body'
|
||||
'/trust:RequestSecurityToken'
|
||||
'/wsp:AppliesTo/wsa:EndpointReference'
|
||||
'/wsa:Address')
|
||||
TO_XPATH = ('/s:Envelope/s:Header'
|
||||
'/a:To')
|
||||
|
||||
TEST_TOKEN = uuid.uuid4().hex
|
||||
|
||||
PROTOCOL = 'saml2'
|
||||
|
||||
@property
|
||||
def _uuid4(self):
|
||||
return '4b911420-4982-4009-8afc-5c596cd487f5'
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticateviaADFSTests, self).setUp()
|
||||
|
||||
self.IDENTITY_PROVIDER = 'adfs'
|
||||
self.IDENTITY_PROVIDER_URL = ('http://adfs.local/adfs/service/trust/13'
|
||||
'/usernamemixed')
|
||||
self.FEDERATION_AUTH_URL = '%s/%s' % (
|
||||
self.TEST_URL,
|
||||
'OS-FEDERATION/identity_providers/adfs/protocols/saml2/auth')
|
||||
self.SP_ENDPOINT = 'https://openstack4.local/Shibboleth.sso/ADFS'
|
||||
|
||||
self.adfsplugin = saml2.V3ADFSPassword(
|
||||
self.TEST_URL, self.IDENTITY_PROVIDER,
|
||||
self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT,
|
||||
self.TEST_USER, self.TEST_TOKEN, self.PROTOCOL)
|
||||
|
||||
self.ADFS_SECURITY_TOKEN_RESPONSE = _load_xml(
|
||||
'ADFS_RequestSecurityTokenResponse.xml')
|
||||
self.ADFS_FAULT = _load_xml('ADFS_fault.xml')
|
||||
|
||||
def test_get_adfs_security_token(self):
|
||||
"""Test ADFSPassword._get_adfs_security_token()."""
|
||||
|
||||
self.requests_mock.post(
|
||||
self.IDENTITY_PROVIDER_URL,
|
||||
content=make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE),
|
||||
status_code=200)
|
||||
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
self.adfsplugin._get_adfs_security_token(self.session)
|
||||
|
||||
adfs_response = etree.tostring(self.adfsplugin.adfs_token)
|
||||
fixture_response = self.ADFS_SECURITY_TOKEN_RESPONSE
|
||||
|
||||
self.assertEqual(fixture_response, adfs_response)
|
||||
|
||||
def test_adfs_request_user(self):
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
user = self.adfsplugin.prepared_request.xpath(
|
||||
self.USER_XPATH, namespaces=self.NAMESPACES)[0]
|
||||
self.assertEqual(self.TEST_USER, user.text)
|
||||
|
||||
def test_adfs_request_password(self):
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
password = self.adfsplugin.prepared_request.xpath(
|
||||
self.PASSWORD_XPATH, namespaces=self.NAMESPACES)[0]
|
||||
self.assertEqual(self.TEST_TOKEN, password.text)
|
||||
|
||||
def test_adfs_request_to(self):
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
to = self.adfsplugin.prepared_request.xpath(
|
||||
self.TO_XPATH, namespaces=self.NAMESPACES)[0]
|
||||
self.assertEqual(self.IDENTITY_PROVIDER_URL, to.text)
|
||||
|
||||
def test_prepare_adfs_request_address(self):
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
address = self.adfsplugin.prepared_request.xpath(
|
||||
self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0]
|
||||
self.assertEqual(self.SP_ENDPOINT, address.text)
|
||||
|
||||
def test_prepare_sp_request(self):
|
||||
assertion = etree.XML(self.ADFS_SECURITY_TOKEN_RESPONSE)
|
||||
assertion = assertion.xpath(
|
||||
saml2.V3ADFSPassword.ADFS_ASSERTION_XPATH,
|
||||
namespaces=saml2.V3ADFSPassword.ADFS_TOKEN_NAMESPACES)
|
||||
assertion = assertion[0]
|
||||
assertion = etree.tostring(assertion)
|
||||
|
||||
assertion = assertion.replace(
|
||||
b'http://docs.oasis-open.org/ws-sx/ws-trust/200512',
|
||||
b'http://schemas.xmlsoap.org/ws/2005/02/trust')
|
||||
assertion = urllib.parse.quote(assertion)
|
||||
assertion = 'wa=wsignin1.0&wresult=' + assertion
|
||||
|
||||
self.adfsplugin.adfs_token = etree.XML(
|
||||
self.ADFS_SECURITY_TOKEN_RESPONSE)
|
||||
self.adfsplugin._prepare_sp_request()
|
||||
|
||||
self.assertEqual(assertion, self.adfsplugin.encoded_assertion)
|
||||
|
||||
def test_get_adfs_security_token_authn_fail(self):
|
||||
"""Test proper parsing XML fault after bad authentication.
|
||||
|
||||
An exceptions.AuthorizationFailure should be raised including
|
||||
error message from the XML message indicating where was the problem.
|
||||
"""
|
||||
self.requests_mock.register_uri('POST',
|
||||
self.IDENTITY_PROVIDER_URL,
|
||||
content=make_oneline(self.ADFS_FAULT),
|
||||
status_code=500)
|
||||
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
self.assertRaises(exceptions.AuthorizationFailure,
|
||||
self.adfsplugin._get_adfs_security_token,
|
||||
self.session)
|
||||
# TODO(marek-denis): Python3 tests complain about missing 'message'
|
||||
# attributes
|
||||
# self.assertEqual('a:FailedAuthentication', e.message)
|
||||
|
||||
def test_get_adfs_security_token_bad_response(self):
|
||||
"""Test proper handling HTTP 500 and mangled (non XML) response.
|
||||
|
||||
This should never happen yet, keystoneauth1 should be prepared
|
||||
and correctly raise exceptions.InternalServerError once it cannot
|
||||
parse XML fault message
|
||||
"""
|
||||
self.requests_mock.register_uri('POST',
|
||||
self.IDENTITY_PROVIDER_URL,
|
||||
content=b'NOT XML',
|
||||
status_code=500)
|
||||
self.adfsplugin._prepare_adfs_request()
|
||||
self.assertRaises(exceptions.InternalServerError,
|
||||
self.adfsplugin._get_adfs_security_token,
|
||||
self.session)
|
||||
|
||||
# TODO(marek-denis): Need to figure out how to properly send cookies
|
||||
# from the request_mock methods.
|
||||
def _send_assertion_to_service_provider(self):
|
||||
"""Test whether SP issues a cookie."""
|
||||
cookie = uuid.uuid4().hex
|
||||
|
||||
self.requests_mock.post(self.SP_ENDPOINT,
|
||||
headers={"set-cookie": cookie},
|
||||
status_code=302)
|
||||
|
||||
self.adfsplugin.adfs_token = self._build_adfs_request()
|
||||
self.adfsplugin._prepare_sp_request()
|
||||
self.adfsplugin._send_assertion_to_service_provider(self.session)
|
||||
|
||||
self.assertEqual(1, len(self.session.session.cookies))
|
||||
|
||||
def test_send_assertion_to_service_provider_bad_status(self):
|
||||
self.requests_mock.register_uri('POST', self.SP_ENDPOINT,
|
||||
status_code=500)
|
||||
|
||||
self.adfsplugin.adfs_token = etree.XML(
|
||||
self.ADFS_SECURITY_TOKEN_RESPONSE)
|
||||
self.adfsplugin._prepare_sp_request()
|
||||
|
||||
self.assertRaises(
|
||||
exceptions.InternalServerError,
|
||||
self.adfsplugin._send_assertion_to_service_provider,
|
||||
self.session)
|
||||
|
||||
def test_access_sp_no_cookies_fail(self):
|
||||
# clean cookie jar
|
||||
self.session.session.cookies = []
|
||||
|
||||
self.assertRaises(exceptions.AuthorizationFailure,
|
||||
self.adfsplugin._access_service_provider,
|
||||
self.session)
|
||||
|
||||
def test_check_valid_token_when_authenticated(self):
|
||||
self.requests_mock.register_uri(
|
||||
'GET', self.FEDERATION_AUTH_URL,
|
||||
json=saml2_fixtures.UNSCOPED_TOKEN,
|
||||
headers=client_fixtures.AUTH_RESPONSE_HEADERS)
|
||||
|
||||
self.session.session.cookies = [object()]
|
||||
self.adfsplugin._access_service_provider(self.session)
|
||||
response = self.adfsplugin.authenticated_response
|
||||
|
||||
self.assertEqual(client_fixtures.AUTH_RESPONSE_HEADERS,
|
||||
response.headers)
|
||||
|
||||
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'],
|
||||
response.json()['token'])
|
||||
|
||||
def test_end_to_end_workflow(self):
|
||||
self.requests_mock.register_uri(
|
||||
'POST', self.IDENTITY_PROVIDER_URL,
|
||||
content=self.ADFS_SECURITY_TOKEN_RESPONSE,
|
||||
status_code=200)
|
||||
self.requests_mock.register_uri(
|
||||
'POST', self.SP_ENDPOINT,
|
||||
headers={"set-cookie": 'x'},
|
||||
status_code=302)
|
||||
self.requests_mock.register_uri(
|
||||
'GET', self.FEDERATION_AUTH_URL,
|
||||
json=saml2_fixtures.UNSCOPED_TOKEN,
|
||||
headers=client_fixtures.AUTH_RESPONSE_HEADERS)
|
||||
|
||||
# NOTE(marek-denis): We need to mimic this until self.requests_mock can
|
||||
# issue cookies properly.
|
||||
self.session.session.cookies = [object()]
|
||||
token = self.adfsplugin.get_auth_ref(self.session)
|
||||
self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token)
|
@ -23,6 +23,10 @@ classifier =
|
||||
packages =
|
||||
keystoneauth1
|
||||
|
||||
[extras]
|
||||
saml2 =
|
||||
lxml>=2.3
|
||||
|
||||
[entry_points]
|
||||
|
||||
keystoneauth1.plugin =
|
||||
|
Loading…
x
Reference in New Issue
Block a user