Utilize serviceValidate endpoint available on CAS 3.0, LDAP attributes available
This commit is contained in:
parent
b7a99e789e
commit
415f5b1d8e
6 changed files with 108 additions and 52 deletions
18
README.md
18
README.md
|
@ -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 ##
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue