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:
Steve Martinelli
2015-02-25 02:11:47 -05:00
parent f845d09b5a
commit a2fc6cf4f4
4 changed files with 175 additions and 0 deletions

View File

@@ -169,3 +169,87 @@ DOMAINS = {
"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>
"""

View File

@@ -25,6 +25,7 @@ from keystoneclient import session
from keystoneclient.tests.unit.v3 import client_fixtures
from keystoneclient.tests.unit.v3 import saml2_fixtures
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__))
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)
self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN)
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')

View File

@@ -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 projects
from keystoneclient.v3.contrib.federation import protocols
from keystoneclient.v3.contrib.federation import saml
from keystoneclient.v3.contrib.federation import service_providers
@@ -26,4 +27,5 @@ class FederationManager(object):
self.protocols = protocols.ProtocolManager(api)
self.projects = projects.ProjectManager(api)
self.domains = domains.DomainManager(api)
self.saml = saml.SamlManager(api)
self.service_providers = service_providers.ServiceProviderManager(api)

View 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