Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgarber/6a49b0400d7ba81fe1cb to your computer and use it in GitHub Desktop.
Save jgarber/6a49b0400d7ba81fe1cb to your computer and use it in GitHub Desktop.
This was just a branch off of master. We're not using the Federated path,
but we are using SSO (it'd be nice if we could make this a separate setting)
"""Logs a user in using a token from Keystone's POST."""
referer = request.META.get('HTTP_REFERER')
auth_url = re.sub(r'/auth.*', '', referer)
- request.federated_login = True
+ request.federated_login = False
request.user = auth.authenticate(request=request, auth_url=auth_url)
auth_user.set_session_from_user(request, request.user)
auth.login(request, request.user)
This works, but it's a bit crufty. We'll clean it up once we've got the api work
sorted out. But if you look at it conceptually, you can see that it's extremely
similar to what's been done in Keystone, but rather than put it in the federated
controller, we've just added the changes to the auth controller
From c999fb584385de906dcd4744701f29711f51cf0e Mon Sep 17 00:00:00 2001
From: Jeff Deville <jeffdeville@gmail.com>
Date: Thu, 19 Mar 2015 16:40:23 -0400
Subject: [PATCH] * add a new auth mechanism called InferredDomain that will
set the REMOTE_DOMAIN info based on the REMOTE_USER value * sso working in
keystone using the remote provider * Update the url for the return call to
live under the mod_openidc plugin's sphere of influence
---
etc/sso_callback_template.html | 22 +++++++++
keystone/auth/controllers.py | 47 +++++++++++++++++--
keystone/auth/plugins/external.py | 6 +++
keystone/auth/routers.py | 6 +++
keystone/common/config.py | 13 +++++-
keystone/tests/default_fixtures.py | 2 +-
keystone/tests/test_auth_plugin.py | 91 +++++++++++++++++++++++++++++++++++-
keystone/tests/test_v3_federation.py | 2 +-
9 files changed, 188 insertions(+), 9 deletions(-)
create mode 100644 etc/sso_callback_template.html
diff --git a/etc/sso_callback_template.html b/etc/sso_callback_template.html
new file mode 100644
index 0000000..3364d69
--- /dev/null
+++ b/etc/sso_callback_template.html
@@ -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>
diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py
index 21e4c9b..7df1106 100644
--- a/keystone/auth/controllers.py
+++ b/keystone/auth/controllers.py
@@ -13,10 +13,12 @@
# under the License.
import sys
-
+import string
from keystoneclient.common import cms
from oslo.utils import timeutils
import six
+from six.moves import urllib
+import webob
from keystone.assignment import controllers as assignment_controllers
from keystone.common import authorization
@@ -361,7 +363,41 @@ class Auth(controller.V3Controller):
super(Auth, self).__init__(*args, **kw)
config.setup_authentication()
- def authenticate_for_token(self, context, auth=None):
+ def sso_auth(self, context, auth=None):
+ 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:
+ # I don't really need token_data, but I'm not sure what's in it either
+ auth = { 'identity': {'methods': [] } }
+ token_id, token_data = self.authenticate_for_token(context, auth=auth, renderToken=False)
+ 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)
+
+ def authenticate_for_token(self, context, auth=None, renderToken=True):
"""Authenticate user and issue a token."""
include_catalog = 'nocatalog' not in context['query_string']
@@ -397,8 +433,11 @@ class Auth(controller.V3Controller):
if trust:
self.trust_api.consume_use(trust['id'])
- return render_token_data_response(token_id, token_data,
- created=True)
+ if renderToken:
+ return render_token_data_response(token_id, token_data,
+ created=True)
+ else:
+ return (token_id, token_data)
except exception.TrustNotFound as e:
raise exception.Unauthorized(e)
diff --git a/keystone/auth/plugins/external.py b/keystone/auth/plugins/external.py
index 3cf51eb..29349bc 100644
--- a/keystone/auth/plugins/external.py
+++ b/keystone/auth/plugins/external.py
@@ -95,6 +95,12 @@ class Domain(Base):
user_ref = self.identity_api.get_user_by_name(username, domain_id)
return user_ref
+@dependency.requires('assignment_api', 'identity_api')
+class InferredDomain(Domain):
+ def _authenticate(self, remote_user, context):
+ context['environment']['REMOTE_DOMAIN'] = remote_user.split("@")[1]
+ return super(InferredDomain, self)._authenticate(remote_user, context)
@dependency.requires('assignment_api', 'identity_api')
class KerberosDomain(Domain):
diff --git a/keystone/auth/routers.py b/keystone/auth/routers.py
index 63b4730..56e25f5 100644
--- a/keystone/auth/routers.py
+++ b/keystone/auth/routers.py
@@ -59,3 +59,9 @@ class Routers(wsgi.RoutersBase):
path='/auth/domains',
get_action='get_auth_domains',
rel=json_home.build_v3_resource_relation('auth_domains'))
+
+ self._add_resource(
+ mapper, auth_controller,
+ path='/auth/OS-FEDERATION/websso/oidc',
+ get_post_action='sso_auth',
+ rel=json_home.build_v3_resource_relation('auth_domains'))
diff --git a/keystone/common/config.py b/keystone/common/config.py
index d7f9dd8..cc59f9d 100644
--- a/keystone/common/config.py
+++ b/keystone/common/config.py
@@ -19,7 +19,7 @@ from oslo import 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 = {
None: [
@@ -464,6 +464,17 @@ FILE_OPTIONS = {
cfg.StrOpt('assertion_prefix', default='',
help='Value to be used when filtering assertion parameters '
'from the environment.'),
+ 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',
diff --git a/keystone/tests/default_fixtures.py b/keystone/tests/default_fixtures.py
index fb8ea04..976b476 100644
--- a/keystone/tests/default_fixtures.py
+++ b/keystone/tests/default_fixtures.py
@@ -49,7 +49,7 @@ TENANTS = [
USERS = [
{
'id': 'foo',
- 'name': 'FOO',
+ 'name': 'FOO@Default@ssosite.com',
'domain_id': DEFAULT_DOMAIN_ID,
'password': 'foo2',
'tenants': ['bar'],
diff --git a/keystone/tests/test_auth_plugin.py b/keystone/tests/test_auth_plugin.py
index 90a3b9e..22fe688 100644
--- a/keystone/tests/test_auth_plugin.py
+++ b/keystone/tests/test_auth_plugin.py
@@ -13,13 +13,16 @@
# under the License.
import uuid
-
import mock
+import os
+from six.moves import urllib
from keystone import auth
from keystone import exception
from keystone import tests
-
+from keystone.tests import core
+from keystone.tests.ksfixtures import database
+from keystone.tests import default_fixtures
# for testing purposes only
METHOD_NAME = 'simple_challenge_response'
@@ -160,6 +163,90 @@ class TestInvalidAuthMethodRegistration(tests.TestCase):
self.assertRaises(ValueError, auth.controllers.load_auth_methods)
+class TestInferredDomain(tests.TestCase):
+ 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)
+ METHOD_NAME = 'keystone.auth.plugins.external.InferredDomain'
+
+ def setUp(self):
+ self.useFixture(database.Database())
+ super(TestInferredDomain, self).setUp()
+
+ self.load_backends()
+ self.load_fixtures(default_fixtures)
+ self.api = auth.controllers.Auth()
+
+ def config_files(self):
+ config_files = super(TestInferredDomain, self).config_files()
+ config_files.append(tests.dirs.tests_conf('test_auth_plugin.conf'))
+ return config_files
+
+ def config_overrides(self):
+ super(TestInferredDomain, self).config_overrides()
+ method_opts = dict(
+ [
+ ('external', 'keystone.auth.plugins.external.InferredDomain'),
+ ('password', 'keystone.auth.plugins.password.Password'),
+ ('token', 'keystone.auth.plugins.token.Token'),
+ ])
+ self.auth_plugin_config_override(
+ methods=['external', 'password', 'token'],
+ **method_opts)
+
+ def test_render_callback_template(self):
+ self.config_fixture.config(
+ group='federation',
+ trusted_dashboard=[self.TRUSTED_DASHBOARD],
+ sso_callback_template=self.SSO_TEMPLATE_PATH)
+ token_id = uuid.uuid4().hex
+ auth_controller = auth.controllers.Auth()
+ resp = auth_controller.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_missing_query(self):
+ environment = {}
+ context = {'environment': environment, 'query_string': []}
+ auth_controller = auth.controllers.Auth()
+ self.assertRaises(exception.ValidationError,
+ auth_controller.sso_auth,
+ context)
+
+ def test_federated_sso_untrusted_dashboard(self):
+ environment = {}
+ context = {
+ 'environment': environment,
+ 'query_string': {'origin': uuid.uuid4().hex},
+ }
+ auth_controller = auth.controllers.Auth()
+ self.assertRaises(exception.Unauthorized,
+ auth_controller.sso_auth,
+ context)
+
+ def test_redirect_from_SSO_login(self):
+ self.config_fixture.config(
+ group='federation',
+ trusted_dashboard=[self.TRUSTED_DASHBOARD],
+ sso_callback_template=self.SSO_TEMPLATE_PATH)
+ context = {
+ 'environment': {
+ 'REMOTE_USER': "FOO@Default@ssosite.com",
+ 'REMOTE_DOMAIN': "default",
+ },
+ 'query_string': {'origin': self.ORIGIN}
+ }
+ # Ok, I think that if
+ auth_controller = auth.controllers.Auth()
+ resp = auth_controller.sso_auth(context)
+ self.assertIn(self.TRUSTED_DASHBOARD, resp.body)
+
+
class TestMapped(tests.TestCase):
def setUp(self):
super(TestMapped, self).setUp()
diff --git a/keystone/tests/test_v3_federation.py b/keystone/tests/test_v3_federation.py
index 202e61c..7447a96 100644
--- a/keystone/tests/test_v3_federation.py
+++ b/keystone/tests/test_v3_federation.py
@@ -37,7 +37,7 @@ from keystone.openstack.common import log
from keystone.tests import federation_fixtures
from keystone.tests import mapping_fixtures
from keystone.tests import test_v3
-
+from oslo_config import fixture as config_fixture
CONF = config.CONF
LOG = log.getLogger(__name__)
--
2.2.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment