Add support to create SAML assertion based on a token
A user should be able to exchange their token for a SAML assertion that is valid on a service provider (the user should must provide this data). implements bp generate-saml-assertions Change-Id: I5cb635929c7f6823ab1e4b1db5e48045be9e0737
This commit is contained in:
@@ -169,3 +169,87 @@ DOMAINS = {
|
|||||||
"next": 'null'
|
"next": 'null'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TOKEN_BASED_SAML = """
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<ns2:Response Destination="http://beta.example.com/Shibboleth.sso/POST/ECP"
|
||||||
|
ID="8c21de08d2f2435c9acf13e72c982846"
|
||||||
|
IssueInstant="2015-03-25T14:43:21Z"
|
||||||
|
Version="2.0">
|
||||||
|
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
|
||||||
|
http://keystone.idp/v3/OS-FEDERATION/saml2/idp
|
||||||
|
</saml:Issuer>
|
||||||
|
<ns2:Status>
|
||||||
|
<ns2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||||
|
</ns2:Status>
|
||||||
|
<saml:Assertion ID="a5f02efb0bff4044b294b4583c7dfc5d"
|
||||||
|
IssueInstant="2015-03-25T14:43:21Z" Version="2.0">
|
||||||
|
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
|
||||||
|
http://keystone.idp/v3/OS-FEDERATION/saml2/idp</saml:Issuer>
|
||||||
|
<xmldsig:Signature>
|
||||||
|
<xmldsig:SignedInfo>
|
||||||
|
<xmldsig:CanonicalizationMethod
|
||||||
|
Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||||
|
<xmldsig:SignatureMethod
|
||||||
|
Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
|
||||||
|
<xmldsig:Reference URI="#a5f02efb0bff4044b294b4583c7dfc5d">
|
||||||
|
<xmldsig:Transforms>
|
||||||
|
<xmldsig:Transform
|
||||||
|
Algorithm="http://www.w3.org/2000/09/xmldsig#
|
||||||
|
enveloped-signature"/>
|
||||||
|
<xmldsig:Transform
|
||||||
|
Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||||
|
</xmldsig:Transforms>
|
||||||
|
<xmldsig:DigestMethod
|
||||||
|
Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
|
||||||
|
<xmldsig:DigestValue>
|
||||||
|
0KH2CxdkfzU+6eiRhTC+mbObUKI=
|
||||||
|
</xmldsig:DigestValue>
|
||||||
|
</xmldsig:Reference>
|
||||||
|
</xmldsig:SignedInfo>
|
||||||
|
<xmldsig:SignatureValue>
|
||||||
|
m2jh5gDvX/1k+4uKtbb08CHp2b9UWsLw
|
||||||
|
</xmldsig:SignatureValue>
|
||||||
|
<xmldsig:KeyInfo>
|
||||||
|
<xmldsig:X509Data>
|
||||||
|
<xmldsig:X509Certificate>...</xmldsig:X509Certificate>
|
||||||
|
</xmldsig:X509Data>
|
||||||
|
</xmldsig:KeyInfo>
|
||||||
|
</xmldsig:Signature>
|
||||||
|
<saml:Subject>
|
||||||
|
<saml:NameID>admin</saml:NameID>
|
||||||
|
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||||
|
<saml:SubjectConfirmationData
|
||||||
|
NotOnOrAfter="2015-03-25T15:43:21.172385Z"
|
||||||
|
Recipient="http://beta.example.com/Shibboleth.sso/POST/ECP"/>
|
||||||
|
</saml:SubjectConfirmation>
|
||||||
|
</saml:Subject>
|
||||||
|
<saml:AuthnStatement AuthnInstant="2015-03-25T14:43:21Z"
|
||||||
|
SessionIndex="9790eb729858456f8a33b7a11f0a637e"
|
||||||
|
SessionNotOnOrAfter="2015-03-25T15:43:21.172385Z">
|
||||||
|
<saml:AuthnContext>
|
||||||
|
<saml:AuthnContextClassRef>
|
||||||
|
urn:oasis:names:tc:SAML:2.0:ac:classes:Password
|
||||||
|
</saml:AuthnContextClassRef>
|
||||||
|
<saml:AuthenticatingAuthority>
|
||||||
|
http://keystone.idp/v3/OS-FEDERATION/saml2/idp
|
||||||
|
</saml:AuthenticatingAuthority>
|
||||||
|
</saml:AuthnContext>
|
||||||
|
</saml:AuthnStatement>
|
||||||
|
<saml:AttributeStatement>
|
||||||
|
<saml:Attribute Name="openstack_user"
|
||||||
|
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
||||||
|
<saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue>
|
||||||
|
</saml:Attribute>
|
||||||
|
<saml:Attribute Name="openstack_roles"
|
||||||
|
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
||||||
|
<saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue>
|
||||||
|
</saml:Attribute>
|
||||||
|
<saml:Attribute Name="openstack_project"
|
||||||
|
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
||||||
|
<saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue>
|
||||||
|
</saml:Attribute>
|
||||||
|
</saml:AttributeStatement>
|
||||||
|
</saml:Assertion>
|
||||||
|
</ns2:Response>
|
||||||
|
"""
|
||||||
|
@@ -25,6 +25,7 @@ from keystoneclient import session
|
|||||||
from keystoneclient.tests.unit.v3 import client_fixtures
|
from keystoneclient.tests.unit.v3 import client_fixtures
|
||||||
from keystoneclient.tests.unit.v3 import saml2_fixtures
|
from keystoneclient.tests.unit.v3 import saml2_fixtures
|
||||||
from keystoneclient.tests.unit.v3 import utils
|
from keystoneclient.tests.unit.v3 import utils
|
||||||
|
from keystoneclient.v3.contrib.federation import saml as saml_manager
|
||||||
|
|
||||||
ROOTDIR = os.path.dirname(os.path.abspath(__file__))
|
ROOTDIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/')
|
XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/')
|
||||||
@@ -621,3 +622,35 @@ class AuthenticateviaADFSTests(utils.TestCase):
|
|||||||
token, token_json = self.adfsplugin._get_unscoped_token(self.session)
|
token, token_json = self.adfsplugin._get_unscoped_token(self.session)
|
||||||
self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN)
|
self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN)
|
||||||
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json)
|
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json)
|
||||||
|
|
||||||
|
|
||||||
|
class SAMLGenerationTests(utils.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SAMLGenerationTests, self).setUp()
|
||||||
|
self.manager = self.client.federation.saml
|
||||||
|
self.SAML2_FULL_URL = ''.join([self.TEST_URL,
|
||||||
|
saml_manager.SAML2_ENDPOINT])
|
||||||
|
|
||||||
|
def test_saml_create(self):
|
||||||
|
"""Test that a token can be exchanged for a SAML assertion."""
|
||||||
|
|
||||||
|
token_id = uuid.uuid4().hex
|
||||||
|
service_provider_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
# Mock the returned text for '/auth/OS-FEDERATION/saml2
|
||||||
|
self.requests_mock.post(self.SAML2_FULL_URL,
|
||||||
|
text=saml2_fixtures.TOKEN_BASED_SAML)
|
||||||
|
|
||||||
|
text = self.manager.create_saml_assertion(service_provider_id,
|
||||||
|
token_id)
|
||||||
|
|
||||||
|
# Ensure returned text is correct
|
||||||
|
self.assertEqual(saml2_fixtures.TOKEN_BASED_SAML, text)
|
||||||
|
|
||||||
|
# Ensure request headers and body are correct
|
||||||
|
req_json = self.requests_mock.last_request.json()
|
||||||
|
self.assertEqual(token_id, req_json['auth']['identity']['token']['id'])
|
||||||
|
self.assertEqual(service_provider_id,
|
||||||
|
req_json['auth']['scope']['service_provider']['id'])
|
||||||
|
self.assertRequestHeaderEqual('Content-Type', 'application/json')
|
||||||
|
@@ -15,6 +15,7 @@ from keystoneclient.v3.contrib.federation import identity_providers
|
|||||||
from keystoneclient.v3.contrib.federation import mappings
|
from keystoneclient.v3.contrib.federation import mappings
|
||||||
from keystoneclient.v3.contrib.federation import projects
|
from keystoneclient.v3.contrib.federation import projects
|
||||||
from keystoneclient.v3.contrib.federation import protocols
|
from keystoneclient.v3.contrib.federation import protocols
|
||||||
|
from keystoneclient.v3.contrib.federation import saml
|
||||||
from keystoneclient.v3.contrib.federation import service_providers
|
from keystoneclient.v3.contrib.federation import service_providers
|
||||||
|
|
||||||
|
|
||||||
@@ -26,4 +27,5 @@ class FederationManager(object):
|
|||||||
self.protocols = protocols.ProtocolManager(api)
|
self.protocols = protocols.ProtocolManager(api)
|
||||||
self.projects = projects.ProjectManager(api)
|
self.projects = projects.ProjectManager(api)
|
||||||
self.domains = domains.DomainManager(api)
|
self.domains = domains.DomainManager(api)
|
||||||
|
self.saml = saml.SamlManager(api)
|
||||||
self.service_providers = service_providers.ServiceProviderManager(api)
|
self.service_providers = service_providers.ServiceProviderManager(api)
|
||||||
|
56
keystoneclient/v3/contrib/federation/saml.py
Normal file
56
keystoneclient/v3/contrib/federation/saml.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 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 keystoneclient import base
|
||||||
|
|
||||||
|
|
||||||
|
SAML2_ENDPOINT = '/auth/OS-FEDERATION/saml2'
|
||||||
|
|
||||||
|
|
||||||
|
class SamlManager(base.Manager):
|
||||||
|
"""Manager class for creating SAML assertions."""
|
||||||
|
|
||||||
|
def create_saml_assertion(self, service_provider, token_id):
|
||||||
|
"""Create a SAML assertion from a token.
|
||||||
|
|
||||||
|
Equivalent Identity API call:
|
||||||
|
POST /auth/OS-FEDERATION/saml2
|
||||||
|
|
||||||
|
:param service_provider: Service Provider resource.
|
||||||
|
:type service_provider: string
|
||||||
|
:param token_id: Token to transform to SAML assertion.
|
||||||
|
:type token_id: string
|
||||||
|
|
||||||
|
:returns: SAML representation of token_id
|
||||||
|
:rtype: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'auth': {
|
||||||
|
'identity': {
|
||||||
|
'methods': ['token'],
|
||||||
|
'token': {
|
||||||
|
'id': token_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'scope': {
|
||||||
|
'service_provider': {
|
||||||
|
'id': base.getid(service_provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
resp, body = self.client.post(SAML2_ENDPOINT, json=body,
|
||||||
|
headers=headers)
|
||||||
|
return resp.text
|
Reference in New Issue
Block a user