Add WebSSO support for federation
Add the ability to return a templated post back HTML response when a call is made that contains info from trusted dashboard host. implements bp websso-portal Co-Authored-By: Jose Castro Leon <jose.castro.leon@cern.ch> Co-Authored-By: Marek Denis <marek.denis@cern.ch> Co-Authored-By: Thai Tran <tqtran@us.ibm.com> Change-Id: Ia5a5c9bd176ea0372c92de6d0a3587d82f2fe5d7
This commit is contained in:
parent
ce701f2771
commit
cd92123a4c
22
etc/sso_callback_template.html
Normal file
22
etc/sso_callback_template.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>Keystone WebSSO redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="sso" name="sso" action="$host" method="post">
|
||||
Please wait...
|
||||
<br/>
|
||||
<input type="hidden" name="token" id="token" value="$token"/>
|
||||
<noscript>
|
||||
<input type="submit" name="submit_no_javascript" id="submit_no_javascript"
|
||||
value="If your JavaScript is disabled, please click to continue"/>
|
||||
</noscript>
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
window.onload = function() {
|
||||
document.forms['sso'].submit();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -19,6 +19,7 @@ import oslo_messaging
|
||||
_DEFAULT_AUTH_METHODS = ['external', 'password', 'token']
|
||||
_CERTFILE = '/etc/keystone/ssl/certs/signing_cert.pem'
|
||||
_KEYFILE = '/etc/keystone/ssl/private/signing_key.pem'
|
||||
_SSO_CALLBACK = '/etc/keystone/sso_callback_template.html'
|
||||
|
||||
|
||||
FILE_OPTIONS = {
|
||||
@ -520,6 +521,17 @@ FILE_OPTIONS = {
|
||||
'unless you really have to. Changing this option '
|
||||
'to empty string or None will not have any impact and '
|
||||
'default name will be used.'),
|
||||
cfg.MultiStrOpt('trusted_dashboard', default=[],
|
||||
help='A list of trusted dashboard hosts. Before '
|
||||
'accepting a Single Sign-On request to return a '
|
||||
'token, the origin host must be a member of the '
|
||||
'trusted_dashboard list. This configuration '
|
||||
'option may be repeated for multiple values. '
|
||||
'For example: trusted_dashboard=http://acme.com '
|
||||
'trusted_dashboard=http://beta.com'),
|
||||
cfg.StrOpt('sso_callback_template', default=_SSO_CALLBACK,
|
||||
help='Location of Single Sign-On callback handler, will '
|
||||
'return a token to a trusted dashboard host.'),
|
||||
],
|
||||
'policy': [
|
||||
cfg.StrOpt('driver',
|
||||
|
@ -13,6 +13,11 @@
|
||||
"""Extensions supporting Federation."""
|
||||
|
||||
from oslo_config import cfg
|
||||
import string
|
||||
|
||||
from oslo_log import log
|
||||
from six.moves import urllib
|
||||
import webob
|
||||
|
||||
from keystone.auth import controllers as auth_controllers
|
||||
from keystone.common import authorization
|
||||
@ -29,6 +34,7 @@ from keystone.models import token_model
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class _ControllerBase(controller.V3Controller):
|
||||
@ -259,6 +265,47 @@ class Auth(auth_controllers.Auth):
|
||||
|
||||
return self.authenticate_for_token(context, auth=auth)
|
||||
|
||||
def federated_sso_auth(self, context, protocol_id):
|
||||
try:
|
||||
remote_id_name = CONF.federation.remote_id_attribute
|
||||
identity_provider = context['environment'][remote_id_name]
|
||||
except KeyError:
|
||||
msg = _('Missing entity ID from environment')
|
||||
LOG.error(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
if 'origin' in context['query_string']:
|
||||
origin = context['query_string'].get('origin')
|
||||
host = urllib.parse.unquote_plus(origin)
|
||||
else:
|
||||
msg = _('Request must have an origin query parameter')
|
||||
LOG.error(msg)
|
||||
raise exception.ValidationError(msg)
|
||||
|
||||
if host in CONF.federation.trusted_dashboard:
|
||||
res = self.federated_authentication(context, identity_provider,
|
||||
protocol_id)
|
||||
token_id = res.headers['X-Subject-Token']
|
||||
return self.render_html_response(host, token_id)
|
||||
else:
|
||||
msg = _('%(host)s is not a trusted dashboard host')
|
||||
msg = msg % {'host': host}
|
||||
LOG.error(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
def render_html_response(self, host, token_id):
|
||||
"""Forms an HTML Form from a template with autosubmit."""
|
||||
|
||||
headers = [('Content-Type', 'text/html')]
|
||||
|
||||
with open(CONF.federation.sso_callback_template) as template:
|
||||
src = string.Template(template.read())
|
||||
|
||||
subs = {'host': host, 'token': token_id}
|
||||
body = src.substitute(subs)
|
||||
return webob.Response(body=body, status='200',
|
||||
headerlist=headers)
|
||||
|
||||
@validation.validated(schema.saml_create, 'auth')
|
||||
def create_saml_assertion(self, context, auth):
|
||||
"""Exchange a scoped token for a SAML assertion.
|
||||
|
@ -77,6 +77,12 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
||||
POST /auth/OS-FEDERATION/saml2
|
||||
GET /OS-FEDERATION/saml2/metadata
|
||||
|
||||
GET /auth/OS-FEDERATION/websso/{protocol_id}
|
||||
?origin=https%3A//horizon.example.com
|
||||
|
||||
POST /auth/OS-FEDERATION/websso/{protocol_id}
|
||||
?origin=https%3A//horizon.example.com
|
||||
|
||||
"""
|
||||
def _construct_url(self, suffix):
|
||||
return "/OS-FEDERATION/%s" % suffix
|
||||
@ -207,6 +213,14 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
||||
path='/auth' + self._construct_url('saml2'),
|
||||
post_action='create_saml_assertion',
|
||||
rel=build_resource_relation(resource_name='saml2'))
|
||||
self._add_resource(
|
||||
mapper, auth_controller,
|
||||
path='/auth' + self._construct_url('websso/{protocol_id}'),
|
||||
get_post_action='federated_sso_auth',
|
||||
rel=build_resource_relation(resource_name='websso'),
|
||||
path_vars={
|
||||
'protocol_id': PROTOCOL_ID_PARAMETER_RELATION,
|
||||
})
|
||||
|
||||
# Keystone-Identity-Provider metadata endpoint
|
||||
self._add_resource(
|
||||
|
@ -24,6 +24,7 @@ from oslotest import mockpatch
|
||||
import saml2
|
||||
from saml2 import saml
|
||||
from saml2 import sigver
|
||||
from six.moves import urllib
|
||||
import xmldsig
|
||||
|
||||
from keystone.auth import controllers as auth_controllers
|
||||
@ -32,6 +33,7 @@ from keystone.contrib.federation import idp as keystone_idp
|
||||
from keystone.contrib.federation import utils as mapping_utils
|
||||
from keystone import exception
|
||||
from keystone import notifications
|
||||
from keystone.tests.unit import core
|
||||
from keystone.tests.unit import federation_fixtures
|
||||
from keystone.tests.unit import mapping_fixtures
|
||||
from keystone.tests.unit import test_v3
|
||||
@ -1937,10 +1939,10 @@ class FederatedTokenTests(FederationTests):
|
||||
self.tokens['ADMIN_ASSERTION'], 'domain',
|
||||
self.domainC['id'])
|
||||
|
||||
def _inject_assertion(self, context, variant):
|
||||
def _inject_assertion(self, context, variant, query_string=None):
|
||||
assertion = getattr(mapping_fixtures, variant)
|
||||
context['environment'].update(assertion)
|
||||
context['query_string'] = []
|
||||
context['query_string'] = query_string or []
|
||||
|
||||
|
||||
class FederatedTokenTestsMethodToken(FederatedTokenTests):
|
||||
@ -2590,3 +2592,64 @@ class ServiceProviderTests(FederationTests):
|
||||
def test_delete_service_provider_404(self):
|
||||
url = self.base_url(suffix=uuid.uuid4().hex)
|
||||
self.delete(url, expected_status=404)
|
||||
|
||||
|
||||
class WebSSOTests(FederatedTokenTests):
|
||||
"""A class for testing Web SSO."""
|
||||
|
||||
SSO_URL = '/auth/OS-FEDERATION/websso/'
|
||||
SSO_TEMPLATE_NAME = 'sso_callback_template.html'
|
||||
SSO_TEMPLATE_PATH = os.path.join(core.dirs.etc(), SSO_TEMPLATE_NAME)
|
||||
TRUSTED_DASHBOARD = 'http://horizon.com'
|
||||
ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD)
|
||||
|
||||
def setUp(self):
|
||||
super(WebSSOTests, self).setUp()
|
||||
self.api = federation_controllers.Auth()
|
||||
|
||||
def config_overrides(self):
|
||||
super(WebSSOTests, self).config_overrides()
|
||||
self.config_fixture.config(
|
||||
group='federation',
|
||||
trusted_dashboard=[self.TRUSTED_DASHBOARD],
|
||||
sso_callback_template=self.SSO_TEMPLATE_PATH,
|
||||
remote_id_attribute=self.REMOTE_ID_ATTR)
|
||||
|
||||
def test_render_callback_template(self):
|
||||
token_id = uuid.uuid4().hex
|
||||
resp = self.api.render_html_response(self.TRUSTED_DASHBOARD, token_id)
|
||||
self.assertIn(token_id, resp.body)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD, resp.body)
|
||||
|
||||
def test_federated_sso_auth(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.IDP}
|
||||
context = {'environment': environment}
|
||||
query_string = {'origin': self.ORIGIN}
|
||||
self._inject_assertion(context, 'EMPLOYEE_ASSERTION', query_string)
|
||||
resp = self.api.federated_sso_auth(context, self.PROTOCOL)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD, resp.body)
|
||||
|
||||
def test_federated_sso_missing_query(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.IDP}
|
||||
context = {'environment': environment}
|
||||
self._inject_assertion(context, 'EMPLOYEE_ASSERTION')
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.api.federated_sso_auth,
|
||||
context, self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_untrusted_dashboard(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.IDP}
|
||||
context = {'environment': environment}
|
||||
query_string = {'origin': uuid.uuid4().hex}
|
||||
self._inject_assertion(context, 'EMPLOYEE_ASSERTION', query_string)
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
self.api.federated_sso_auth,
|
||||
context, self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_missing_remote_id(self):
|
||||
context = {'environment': {}}
|
||||
query_string = {'origin': self.ORIGIN}
|
||||
self._inject_assertion(context, 'EMPLOYEE_ASSERTION', query_string)
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
self.api.federated_sso_auth,
|
||||
context, self.PROTOCOL)
|
||||
|
@ -312,6 +312,10 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = {
|
||||
json_home.build_v3_resource_relation('users'): {'href': '/users'},
|
||||
_build_federation_rel(resource_name='domains'): {
|
||||
'href': '/OS-FEDERATION/domains'},
|
||||
_build_federation_rel(resource_name='websso'): {
|
||||
'href-template': '/auth/OS-FEDERATION/websso/{protocol_id}',
|
||||
'href-vars': {
|
||||
'protocol_id': PROTOCOL_ID_PARAM_RELATION, }},
|
||||
_build_federation_rel(resource_name='projects'): {
|
||||
'href': '/OS-FEDERATION/projects'},
|
||||
_build_federation_rel(resource_name='saml2'): {
|
||||
|
Loading…
Reference in New Issue
Block a user