Merge pull request #26 from nelsonw2014/master
Utilize serviceValidate endpoint available on CAS 3.0
This commit is contained in:
commit
6deda82869
7 changed files with 121 additions and 44 deletions
26
README.md
26
README.md
|
@ -4,7 +4,7 @@ Flask-CAS
|
|||
[](https://travis-ci.org/cameronbwhite/Flask-CAS)
|
||||
|
||||
Flask-CAS is a Flask extension which makes it easy to
|
||||
authenticate with a CAS.
|
||||
authenticate with a CAS Server (v2.0+).
|
||||
|
||||
## What is CAS? ##
|
||||
|
||||
|
@ -81,10 +81,12 @@ The `/login/` route will redirect the user to the CAS specified by the
|
|||
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`
|
||||
configuration value.
|
||||
configuration value. If `attributes` are available, they will be stored
|
||||
in the session under the key specified by the
|
||||
`CAS_ATTRIBUTES_SESSION_KEY`
|
||||
|
||||
The `/logout/` route will redirect the user to the CAS logout page and
|
||||
the `username` will be removed from the session.
|
||||
the `username` and `attributes` will be removed from the session.
|
||||
|
||||
For convenience you can use the `cas.login` and `cas.logout`
|
||||
functions to redirect users to the login and logout pages.
|
||||
|
@ -117,14 +119,15 @@ 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_USERNAME_SESSION_KEY | CAS_USERNAME |
|
||||
|CAS_ATTRIBUTES_SESSION_KEY | CAS_ATTRIBUTES |
|
||||
|CAS_LOGIN_ROUTE | '/cas' |
|
||||
|CAS_LOGOUT_ROUTE | '/cas/logout' |
|
||||
|CAS_VALIDATE_ROUTE | '/cas/serviceValidate'|
|
||||
|CAS_AFTER_LOGOUT | None |
|
||||
|
||||
## Example ##
|
||||
|
||||
|
@ -145,5 +148,6 @@ def route_root():
|
|||
return flask.render_template(
|
||||
'layout.html',
|
||||
username = cas.username,
|
||||
display_name = cas.attributes['cas:displayName']
|
||||
)
|
||||
```
|
||||
|
|
|
@ -28,14 +28,15 @@ 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_USERNAME_SESSION_KEY | CAS_USERNAME |
|
||||
|CAS_ATTRIBUTES_SESSION_KEY | CAS_ATTRIBUTES |
|
||||
|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):
|
||||
|
@ -47,9 +48,11 @@ class CAS(object):
|
|||
# Configuration defaults
|
||||
app.config.setdefault('CAS_TOKEN_SESSION_KEY', '_CAS_TOKEN')
|
||||
app.config.setdefault('CAS_USERNAME_SESSION_KEY', 'CAS_USERNAME')
|
||||
app.config.setdefault('CAS_ATTRIBUTES_SESSION_KEY', 'CAS_ATTRIBUTES')
|
||||
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 2.0
|
||||
app.config.setdefault('CAS_AFTER_LOGOUT', None)
|
||||
# Register Blueprint
|
||||
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
|
||||
|
@ -70,8 +73,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_USERNAME_SESSION_KEY'] in flask.session:
|
||||
return flask.session.get(self.app.config['CAS_USERNAME_SESSION_KEY'])
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
if self.app.config['CAS_ATTRIBUTES_SESSION_KEY'] in flask.session:
|
||||
return flask.session.get(self.app.config['CAS_ATTRIBUTES_SESSION_KEY'])
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
|
|
|
@ -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,9 @@ 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_USERNAME_SESSION_KEY` and
|
||||
the user's attributes are saved under the key
|
||||
'CAS_USERNAME_ATTRIBUTE_KEY'
|
||||
"""
|
||||
|
||||
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
|
||||
|
@ -60,10 +63,14 @@ def logout():
|
|||
"""
|
||||
|
||||
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
|
||||
cas_attributes_session_key = current_app.config['CAS_ATTRIBUTES_SESSION_KEY']
|
||||
|
||||
if cas_username_session_key in flask.session:
|
||||
del flask.session[cas_username_session_key]
|
||||
|
||||
if cas_attributes_session_key in flask.session:
|
||||
del flask.session[cas_attributes_session_key]
|
||||
|
||||
if(current_app.config['CAS_AFTER_LOGOUT'] != None):
|
||||
redirect_url = create_cas_logout_url(
|
||||
current_app.config['CAS_SERVER'],
|
||||
|
@ -83,10 +90,12 @@ 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_USERNAME_SESSION_KEY` while tha validated attributes dictionary
|
||||
is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'.
|
||||
"""
|
||||
|
||||
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
|
||||
cas_attributes_session_key = current_app.config['CAS_ATTRIBUTES_SESSION_KEY']
|
||||
|
||||
current_app.logger.debug("validating token {0}".format(ticket))
|
||||
|
||||
|
@ -99,17 +108,29 @@ 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")
|
||||
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_username_session_key] = username
|
||||
flask.session[cas_attributes_session_key] = attributes
|
||||
else:
|
||||
current_app.logger.debug("invalid")
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -73,6 +73,7 @@ if __name__ == "__main__":
|
|||
],
|
||||
install_requires = [
|
||||
"Flask",
|
||||
"xmltodict",
|
||||
],
|
||||
test_requires = [
|
||||
"Nose",
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -28,10 +28,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_ATTRIBUTES_SESSION_KEY'] = 'CAS_ATTRIBUTES'
|
||||
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 +47,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,8 +72,15 @@ 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:
|
||||
|
@ -70,6 +88,8 @@ class test_routing(unittest.TestCase):
|
|||
client.get('/login/')
|
||||
self.assertTrue(
|
||||
self.app.config['CAS_USERNAME_SESSION_KEY'] not in flask.session)
|
||||
self.assertTrue(
|
||||
self.app.config['CAS_ATTRIBUTES_SESSION_KEY'] not in flask.session)
|
||||
self.assertTrue(
|
||||
self.app.config['CAS_TOKEN_SESSION_KEY'] not in flask.session)
|
||||
|
||||
|
@ -105,8 +125,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 +145,21 @@ 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.assertTrue(
|
||||
self.app.config['CAS_ATTRIBUTES_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