Utilize serviceValidate endpoint available on CAS 3.0, LDAP attributes available

This commit is contained in:
Warren Nelson 2015-08-04 16:47:14 -04:00
parent b7a99e789e
commit 415f5b1d8e
6 changed files with 108 additions and 52 deletions

View file

@ -80,7 +80,7 @@ The `/login/` route will redirect the user to the CAS specified by the
`CAS_SERVER` configuration value. If login is successful the user will
be redirect to the endpoint specified by the `CAS_AFTER_LOGIN`
configuration value, and the logged in user's `username` will be store
in the session under the key specified by the `CAS_USERNAME_SESSION_KEY`
in the session under the key specified by the `CAS_USER_SESSION_KEY`
configuration value.
The `/logout/` route will redirect the user to the CAS logout page and
@ -117,14 +117,14 @@ you may use the `cas.login_required` method.
#### Optional Configs ####
|Key | Default |
|-------------------------|----------------|
|CAS_TOKEN_SESSION_KEY | _CAS_TOKEN |
|CAS_USERNAME_SESSION_KEY | CAS_USERNAME |
|CAS_LOGIN_ROUTE | '/cas' |
|CAS_LOGOUT_ROUTE | '/cas/logout' |
|CAS_VALIDATE_ROUTE | '/cas/validate'|
|CAS_AFTER_LOGOUT | None |
|Key | Default |
|-------------------------|-----------------------|
|CAS_TOKEN_SESSION_KEY | _CAS_TOKEN |
|CAS_USER_SESSION_KEY | CAS_USER |
|CAS_LOGIN_ROUTE | '/cas' |
|CAS_LOGOUT_ROUTE | '/cas/logout' |
|CAS_VALIDATE_ROUTE | '/cas/serviceValidate'|
|CAS_AFTER_LOGOUT | None |
## Example ##

View file

@ -28,14 +28,14 @@ class CAS(object):
Optional Configs:
|Key | Default |
|-------------------------|----------------|
|CAS_TOKEN_SESSION_KEY | _CAS_TOKEN |
|CAS_USERNAME_SESSION_KEY | CAS_USERNAME |
|CAS_LOGIN_ROUTE | '/cas' |
|CAS_LOGOUT_ROUTE | '/cas/logout' |
|CAS_VALIDATE_ROUTE | '/cas/validate'|
|CAS_AFTER_LOGOUT | None |
|Key | Default |
|-------------------------|-----------------------|
|CAS_TOKEN_SESSION_KEY | _CAS_TOKEN |
|CAS_USER_SESSION_KEY | CAS_USER |
|CAS_LOGIN_ROUTE | '/cas' |
|CAS_LOGOUT_ROUTE | '/cas/logout' |
|CAS_VALIDATE_ROUTE | '/cas/serviceValidate'|
|CAS_AFTER_LOGOUT | None |
"""
def __init__(self, app=None, url_prefix=None):
@ -46,10 +46,11 @@ class CAS(object):
def init_app(self, app, url_prefix=None):
# Configuration defaults
app.config.setdefault('CAS_TOKEN_SESSION_KEY', '_CAS_TOKEN')
app.config.setdefault('CAS_USERNAME_SESSION_KEY', 'CAS_USERNAME')
app.config.setdefault('CAS_USER_SESSION_KEY', 'CAS_USER')
app.config.setdefault('CAS_LOGIN_ROUTE', '/cas')
app.config.setdefault('CAS_LOGOUT_ROUTE', '/cas/logout')
app.config.setdefault('CAS_VALIDATE_ROUTE', '/cas/validate')
app.config.setdefault('CAS_VALIDATE_ROUTE', '/cas/serviceValidate')
# Requires CAS 3.0
app.config.setdefault('CAS_AFTER_LOGOUT', None)
# Register Blueprint
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
@ -70,8 +71,17 @@ class CAS(object):
@property
def username(self):
return flask.session.get(
self.app.config['CAS_USERNAME_SESSION_KEY'], None)
if self.app.config['CAS_USER_SESSION_KEY'] in flask.session:
return flask.session.get(self.app.config['CAS_USER_SESSION_KEY'])[0]
else:
return None
@property
def attribute(self):
if self.app.config['CAS_USER_SESSION_KEY'] in flask.session:
return flask.session.get(self.app.config['CAS_USER_SESSION_KEY'])[1]
else:
return None
@property
def token(self):
@ -87,7 +97,7 @@ def logout():
def login_required(function):
@wraps(function)
def wrap(*args, **kwargs):
if 'CAS_USERNAME' not in flask.session:
if 'CAS_USER' not in flask.session:
flask.session['CAS_AFTER_LOGIN_SESSION_URL'] = flask.request.path
return login()
else:

View file

@ -103,7 +103,7 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket,
Keyword arguments:
cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
cas_route -- The route where the CAS lives on server (ex. /cas/validate)
cas_route -- The route where the CAS lives on server (ex. /cas/serviceValidate)
service -- (ex. http://localhost:5000/login)
ticket -- (ex. 'ST-58274-x839euFek492ou832Eena7ee-cas')
renew -- "true" or "false"
@ -111,11 +111,11 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket,
Example usage:
>>> create_cas_validate_url(
... 'http://sso.pdx.edu',
... '/cas/validate',
... '/cas/serviceValidate',
... 'http://localhost:5000/login',
... 'ST-58274-x839euFek492ou832Eena7ee-cas'
... )
'http://sso.pdx.edu/cas/validate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas'
'http://sso.pdx.edu/cas/serviceValidate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas'
"""
return create_url(
cas_url,

View file

@ -1,4 +1,5 @@
import flask
from xmltodict import parse
from flask import current_app
from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url
@ -24,7 +25,7 @@ def login():
to login. If the login was successful, the CAS will respond to this
route with the ticket in the url. The ticket is then validated.
If validation was successful the logged in username is saved in
the user's session under the key `CAS_USERNAME_SESSION_KEY`.
the user's session under the key `CAS_USER_SESSION_KEY`.
"""
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
@ -59,10 +60,10 @@ def logout():
When the user accesses this route they are logged out.
"""
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
cas_user_session_key = current_app.config['CAS_USER_SESSION_KEY']
if cas_username_session_key in flask.session:
del flask.session[cas_username_session_key]
if cas_user_session_key in flask.session:
del flask.session[cas_user_session_key]
if(current_app.config['CAS_AFTER_LOGOUT'] != None):
redirect_url = create_cas_logout_url(
@ -83,10 +84,10 @@ def validate(ticket):
Will attempt to validate the ticket. If validation fails, then False
is returned. If validation is successful, then True is returned
and the validated username is saved in the session under the
key `CAS_USERNAME_SESSION_KEY`.
key `CAS_USER_SESSION_KEY`.
"""
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
cas_user_session_key = current_app.config['CAS_USER_SESSION_KEY']
current_app.logger.debug("validating token {0}".format(ticket))
@ -99,17 +100,28 @@ def validate(ticket):
current_app.logger.debug("Making GET request to {0}".format(
cas_validate_url))
xml_from_dict = {}
isValid = False
try:
(isValid, username) = urlopen(cas_validate_url).readlines()
isValid = True if isValid.strip() == b'yes' else False
username = username.strip().decode('utf8', 'ignore')
xmldump = urlopen(cas_validate_url).read().strip().decode('utf8', 'ignore')
xml_from_dict = parse(xmldump)
isValid = True if "cas:authenticationSuccess" in xml_from_dict["cas:serviceResponse"] else False
except ValueError:
current_app.logger.error("CAS returned unexpected result")
isValid = False
if isValid:
current_app.logger.debug("valid")
flask.session[cas_username_session_key] = username
xml_from_dict = xml_from_dict["cas:serviceResponse"]["cas:authenticationSuccess"]
username = xml_from_dict["cas:user"]
attributes = xml_from_dict["cas:attributes"]
if "cas:memberOf" in attributes:
attributes["cas:memberOf"] = attributes["cas:memberOf"].lstrip('[').rstrip(']').split(',')
for group_number in range(0, len(attributes['cas:memberOf'])):
attributes['cas:memberOf'][group_number] = attributes['cas:memberOf'][group_number].lstrip(' ').rstrip(' ')
flask.session[cas_user_session_key] = (username, attributes)
else:
current_app.logger.debug("invalid")

View file

@ -184,21 +184,21 @@ class test_create_cas_validate_url(unittest.TestCase):
self.assertEqual(
create_cas_validate_url(
'http://sso.pdx.edu',
'/cas/validate',
'/cas/serviceValidate',
'http://localhost:5000/login',
'ST-58274-x839euFek492ou832Eena7ee-cas'
),
'http://sso.pdx.edu/cas/validate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas'
'http://sso.pdx.edu/cas/serviceValidate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas'
)
def test_with_renew(self):
self.assertEqual(
create_cas_validate_url(
'http://sso.pdx.edu',
'/cas/validate',
'/cas/serviceValidate',
'http://localhost:5000/login',
'ST-58274-x839euFek492ou832Eena7ee-cas',
renew='true',
),
'http://sso.pdx.edu/cas/validate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas&renew=true'
'http://sso.pdx.edu/cas/serviceValidate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas&renew=true'
)

View file

@ -27,11 +27,11 @@ class test_routing(unittest.TestCase):
self.app.config['CAS_SERVER'] = 'http://cas.server.com'
self.app.config['CAS_TOKEN_SESSION_KEY'] = '_CAS_TOKEN'
self.app.config['CAS_USERNAME_SESSION_KEY'] = 'CAS_USERNAME'
self.app.config['CAS_USER_SESSION_KEY'] = 'CAS_USER'
self.app.config['CAS_AFTER_LOGIN'] = 'root'
self.app.config['CAS_LOGIN_ROUTE'] = '/cas'
self.app.config['CAS_LOGOUT_ROUTE'] = '/cas/logout'
self.app.config['CAS_VALIDATE_ROUTE'] = '/cas/validate'
self.app.config['CAS_VALIDATE_ROUTE'] = '/cas/serviceValidate'
self.app.config['CAS_AFTER_LOGOUT'] = 'http://localhost:5000'
def test_setUp(self):
@ -46,8 +46,18 @@ class test_routing(unittest.TestCase):
'http://cas.server.com/cas?service=http%3A%2F%2Flocalhost%2Flogin%2F')
@mock.patch.object(routing, 'urlopen',
return_value=io.BytesIO(b'yes\nbob\n'))
def test_login_by_logged_in_user_valid(self, m):
return_value=io.BytesIO(b'\n\n'))
@mock.patch.object(routing, 'parse',
return_value={
"cas:serviceResponse": {
"cas:authenticationSuccess": {
"cas:user": "bob",
"cas:attributes": {
}
}
}
})
def test_login_by_logged_in_user_valid(self, m, n):
ticket = '12345-abcdefg-cas'
with self.app.test_client() as client:
with client.session_transaction() as s:
@ -61,15 +71,22 @@ class test_routing(unittest.TestCase):
ticket)
@mock.patch.object(routing, 'urlopen',
return_value=io.BytesIO(b'no\n\n'))
def test_login_by_logged_in_user_invalid(self, m):
return_value=io.BytesIO(b'\n\n'))
@mock.patch.object(routing, 'parse',
return_value={
"cas:serviceResponse": {
'cas:authenticationFailure': {
}
}
})
def test_login_by_logged_in_user_invalid(self, m, n):
ticket = '12345-abcdefg-cas'
with self.app.test_client() as client:
with client.session_transaction() as s:
s[self.app.config['CAS_TOKEN_SESSION_KEY']] = ticket
client.get('/login/')
self.assertTrue(
self.app.config['CAS_USERNAME_SESSION_KEY'] not in flask.session)
self.app.config['CAS_USER_SESSION_KEY'] not in flask.session)
self.assertTrue(
self.app.config['CAS_TOKEN_SESSION_KEY'] not in flask.session)
@ -105,8 +122,18 @@ class test_routing(unittest.TestCase):
'http://cas.server.com/cas/logout?service=http%3A%2F%2Flocalhost%3A5000')
@mock.patch.object(routing, 'urlopen',
return_value=io.BytesIO(b'yes\nbob\n'))
def test_validate_valid(self, m):
return_value=io.BytesIO(b'\n\n'))
@mock.patch.object(routing, 'parse',
return_value={
"cas:serviceResponse": {
"cas:authenticationSuccess": {
"cas:user": "bob",
"cas:attributes": {
}
}
}
})
def test_validate_valid(self, m, n):
with self.app.test_request_context('/login/'):
ticket = '12345-abcdefg-cas'
self.assertEqual(routing.validate(ticket), True)
@ -115,12 +142,19 @@ class test_routing(unittest.TestCase):
'bob')
@mock.patch.object(routing, 'urlopen',
return_value=io.BytesIO(b'no\n\n'))
def test_validate_invalid(self, m):
return_value=io.BytesIO(b'\n\n'))
@mock.patch.object(routing, 'parse',
return_value={
"cas:serviceResponse": {
'cas:authenticationFailure': {
}
}
})
def test_validate_invalid(self, m, n):
with self.app.test_request_context('/login/'):
ticket = '12345-abcdefg-cas'
self.assertEqual(routing.validate(ticket), False)
self.assertTrue(
self.app.config['CAS_USERNAME_SESSION_KEY'] not in flask.session)
self.app.config['CAS_USER_SESSION_KEY'] not in flask.session)
self.assertTrue(
self.app.config['CAS_TOKEN_SESSION_KEY'] not in flask.session)