Skip to content

Commit

Permalink
Merge pull request #18 from kstateome/develop
Browse files Browse the repository at this point in the history
Release 0.9.0
  • Loading branch information
gpennington committed Sep 13, 2013
2 parents 494145c + 0e52ac9 commit 847ade1
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 31 deletions.
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
# django-cas

CAS client for Django. This is K-State's fork of the original, which lives at
https://bitbucket.org/cpcc/django-cas/overview. This fork is actively maintaned and
includes several new features.
CAS client for Django.

Current version: 0.8.5
Current version: 0.9.0

https://github.com/kstateome/django-cas
This is [K-State's fork](https://github.com/kstateome/django-cas) of [the original](https://bitbucket.org/cpcc/django-cas/overview.) and includes [several additional features](https://github.com/kstateome/django-cas/edit/develop/README.md#additional-features) as well as features merged from

* [KTHse's django-cas2](https://github.com/KTHse/django-cas2).
* [Edmund Crewe's proxy ticket patch](http://code.google.com/r/edmundcrewe-proxypatch/source/browse/django-cas-proxy.patch).


## Install

``pip install git+ssh://[email protected]/kstateome/[email protected]#egg=cas``

See the document at Bitbucket

https://bitbucket.org/cpcc/django-cas/overview

## How to Contribute

Fork and branch off of the ``develop`` branch. Submit Pull requests back to ``kstateome:develop``.

## Settings.py for CAS

Add the following to middleware if you want to use CAS::

MIDDLEWARE_CLASSES = (
'cas.middleware.CASMiddleware',
)


Add these to ``settings.py`` to use the CAS Backend::


CAS_SERVER_URL = "Your Cas Server"
CAS_LOGOUT_COMPLETELY = True
CAS_PROVIDE_URL_TO_LOGOUT = True

# Additional Features

This fork contains additional features not found in the original:
* Proxied Hosts
* CAS Response Callbacks
* CAS Gateway
* Proxy Tickets (From Edmund Crewe)
* Proxy Tickets (From Edmund Crewe)

## Proxied Hosts

Expand All @@ -55,7 +62,7 @@ This middleware needs to be added before the django ``common`` middleware.
## CAS Response Callbacks

To store data from CAS, create a callback function that accepts the ElementTree object from the
proxyValidate response. There can be multiple callbacks, and they can live anywhere. Define the
proxyValidate response. There can be multiple callbacks, and they can live anywhere. Define the
callback(s) in ``settings.py``:

CAS_RESPONSE_CALLBACKS = (
Expand All @@ -74,7 +81,7 @@ and create the functions in ``path/to/module.py``:
profile.email = tree[0][1].text
profile.position = tree[0][2].text
profile.save()


## CAS Gateway

Expand All @@ -101,5 +108,5 @@ a generic ``HttpResponseForbidden`` will be returned.

## Proxy Tickets

This fork also includes Edmund Crewe's proxy ticket patch:
http://code.google.com/r/edmundcrewe-proxypatch/source/browse/django-cas-proxy.patch
This fork also includes
[Edmund Crewe's proxy ticket patch](http://code.google.com/r/edmundcrewe-proxypatch/source/browse/django-cas-proxy.patch).
71 changes: 61 additions & 10 deletions cas/backends.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
"""CAS authentication backend"""

import logging
from urllib import urlencode, urlopen
from urlparse import urljoin
from xml.dom import minidom

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from cas.exceptions import CasTicketException
from cas.models import User, Tgt, PgtIOU
from cas.utils import cas_response_callbacks

__all__ = ['CASBackend']

logger = logging.getLogger(__name__)

def _verify_cas1(ticket, service):
"""Verifies CAS 1.0 authentication ticket.
Expand Down Expand Up @@ -41,34 +46,60 @@ def _verify_cas2(ticket, service):
except ImportError:
from elementtree import ElementTree

params = {'ticket': ticket, 'service': service}
if settings.CAS_PROXY_CALLBACK:
params = {'ticket': ticket, 'service': service, 'pgtUrl': settings.CAS_PROXY_CALLBACK}
else:
params = {'ticket': ticket, 'service': service}
params['pgtUrl'] = settings.CAS_PROXY_CALLBACK

url = (urljoin(settings.CAS_SERVER_URL, 'proxyValidate') + '?' +
urlencode(params))

page = urlopen(url)

username = None

try:
response = page.read()
tree = ElementTree.fromstring(response)
document = minidom.parseString(response)

#Useful for debugging
#from xml.dom.minidom import parseString
#from xml.etree import ElementTree
#txt = ElementTree.tostring(tree)
#print parseString(txt).toprettyxml()

#print document.toprettyxml()

if tree[0].tag.endswith('authenticationSuccess'):
if settings.CAS_RESPONSE_CALLBACKS:
cas_response_callbacks(tree)
return tree[0][0].text

username = tree[0][0].text

pgt_el = document.getElementsByTagName('cas:proxyGrantingTicket')
if pgt_el:
pgt = pgt_el[0].firstChild.nodeValue
try:
pgtIou = _get_pgtiou(pgt)
tgt = Tgt.objects.get(username=username)
tgt.tgt = pgtIou.tgt
tgt.save()
pgtIou.delete()
except Tgt.DoesNotExist:
Tgt.objects.create(username=username, tgt=pgtIou.tgt)
pgtIou.delete()
except Exception:
logger.error('Failed to do proxy authentication.')

else:
return None
failure = document.getElementsByTagName('cas:authenticationFailure')
if failure:
logger.warn('Authentication failed from CAS server: %s',
failure[0].firstChild.nodeValue)

except Exception as e:
logger.error('Failed to verify CAS authentication', e)

finally:
page.close()

return username


def verify_proxy_ticket(ticket, service):
"""Verifies CAS 2.0+ XML-based proxy ticket.
Expand Down Expand Up @@ -111,6 +142,26 @@ def verify_proxy_ticket(ticket, service):
_verify = _PROTOCOLS[settings.CAS_VERSION]


def _get_pgtiou(pgt):
""" Returns a PgtIOU object given a pgt.
The PgtIOU (tgt) is set by the CAS server in a different request
that has completed before this call, however, it may not be found in
the database by this calling thread, hence the attempt to get the
ticket is retried for up to 5 seconds. This should be handled some
better way.
"""
pgtIou = None
retries_left = 5
while not pgtIou and retries_left:
try:
return PgtIOU.objects.get(tgt=pgt)
except PgtIOU.DoesNotExist:
time.sleep(1)
retries_left -= 1
raise CasTicketException("Could not find pgtIou for pgt %s" % pgt)


class CASBackend(object):
"""CAS authentication backend"""

Expand Down
3 changes: 2 additions & 1 deletion cas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def get_proxy_ticket_for(self, service):
if tree[0].tag.endswith('proxySuccess'):
return tree[0][0].text
else:
raise CasTicketException("Failed to get proxy ticket")
raise CasTicketException('Failed to get proxy ticket: %s' % \
tree[0].text.strip())
finally:
page.close()

Expand Down
17 changes: 9 additions & 8 deletions cas/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
import urlparse
from operator import itemgetter

from django.http import get_host, HttpResponseRedirect, HttpResponseForbidden, HttpResponse
from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from cas.models import PgtIOU
from django.contrib import messages

__all__ = ['login', 'logout']


def _service_url(request, redirect_to=None, gateway=False):
"""Generates application service URL for CAS"""

protocol = ('http://', 'https://')[request.is_secure()]
host = get_host(request)
host = request.get_host()
prefix = (('http://', 'https://')[request.is_secure()] + host)
service = protocol + host + request.path
if redirect_to:
Expand Down Expand Up @@ -61,7 +62,7 @@ def _redirect_url(request):
else:
next = request.META.get('HTTP_REFERER', settings.CAS_REDIRECT_URL)

host = get_host(request)
host = request.get_host()
prefix = (('http://', 'https://')[request.is_secure()] + host)
if next.startswith(prefix):
next = next[len(prefix):]
Expand All @@ -73,7 +74,7 @@ def _login_url(service, ticket='ST', gateway=False):
LOGINS = {'ST':'login',
'PT':'proxyValidate'}
if gateway:
params = {'service': service, 'gateway':True}
params = {'service': service, 'gateway': 'true'}
else:
params = {'service': service}
if settings.CAS_EXTRA_LOGIN_PARAMS:
Expand All @@ -88,9 +89,9 @@ def _logout_url(request, next_page=None):
"""Generates CAS logout URL"""

url = urlparse.urljoin(settings.CAS_SERVER_URL, 'logout')
if next_page:
if next_page and getattr(settings, 'CAS_PROVIDE_URL_TO_LOGOUT', True):
protocol = ('http://', 'https://')[request.is_secure()]
host = get_host(request)
host = request.get_host()
url += '?' + urlencode({'url': protocol + host + next_page})
return url

Expand Down Expand Up @@ -130,7 +131,7 @@ def login(request, next_page=None, required=False, gateway=False):
#Has ticket, not session
if getattr(settings, 'CAS_CUSTOM_FORBIDDEN'):
from django.core.urlresolvers import reverse
return HttpResponseRedirect(reverse(settings.CAS_CUSTOM_FORBIDDEN)+ "?" + request.META['QUERY_STRING'] )
return HttpResponseRedirect(reverse(settings.CAS_CUSTOM_FORBIDDEN) + "?" + request.META['QUERY_STRING'])
else:
error = "<h1>Forbidden</h1><p>Login failed.</p>"
return HttpResponseForbidden(error)
Expand Down Expand Up @@ -167,7 +168,7 @@ def proxy_callback(request):
if not (pgtIou and tgt):
return HttpResponse('No pgtIOO', mimetype="text/plain")
try:
PgtIOU.objects.create(tgt = tgt, pgtIou = pgtIou, created = datetime.now())
PgtIOU.objects.create(tgt=tgt, pgtIou=pgtIou, created=datetime.now())
request.session['pgt-TICKET'] = ticket
return HttpResponse('PGT ticket is: %s' % str(ticket, mimetype="text/plain"))
except:
Expand Down

0 comments on commit 847ade1

Please sign in to comment.