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' "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 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')

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 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)

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