How I Got Grok Talking To CAS

Author:Brandon Craig Rhodes
Version:unkown

One user’s experience connecting his Grok application to a CAS authentication web server, which he probably did the wrong way around, but which he’s sharing because at least it worked.

Might be outdated! This document hasn’t been reviewed for Grok 1.0 and may be outdated. If you would like to review the document, please read this post.

Purpose

Our campus uses a CAS server to provide single-sign-on across web applications for our users. How can a Grok application send its users to a CAS server to make them log in?

Well, knowing almost nothing about Grok or Zope, I had to put together something simple fairly quickly. In particular, this solution does not integrate with the standard and pluggable Zope authentication mechanism; instead, it does its own thing. Perhaps wiser and more experienced Zope folks can write another HOWTO explaining how to do this right! But until then, people were asking for something — anything — that would give them information with which they could get started.

The Three Steps

There were three steps necessary to get Grok working with CAS. They each wound up getting their own .py file the way I did it.

  • auth.py

This file contains a primitive method for recording, in a user’s cookie-based session, which CAS user they’ve authenticated themselves as (if any), and for doling that information back out when various parts of Zope call for an IAuthentication local utility.

  • login.py

This file, and its accompanying template named login_templates/login.pt, creates a very simple (and, to be frank, fairly ugly) login screen. The screen consists of a single button that sends you to Webauth.

  • app.py

Finally, you’ll need to add one or two statements to your main app.py file in order to register the above-mentioned adapter as a local utility.

Wow, that really sounds clunky, doesn’t it? And it occurs to me that, the way I’ve done it, the login screen doesn’t remember where you were when you asked to log in, so once you’re logged in you’ve got to navigate back to the page you came from all by yourself. I guess we’d better improve this scheme soon.

Remembering Who Has Authenticated

How can Grok remember who is authenticated to your site? The most convenient way to store this information is through a wonderful mechanism Zope 3 provides called ISession. The magic of this adapter is that Grok, without even being asked, takes it upon itself to mark with a cookie every user that visits your site, and then keep up with them as they move from page to page. At any time you can invoke the ISession utility in order to access a bundle of behind-the- scenes information that Grok will remember for you between the user’s page visits.

And so, here is my auth.py file. It provides one or two simple functions for getting and setting a username in the user’s session where they’ll be safely stored, and then an IAuthentication utility that can spring into action when any part of the frameworks wants to know who is connected while the user is browsing inside of your Grok application:

import grok
from zope.session.interfaces import ISession
from zope.app.security.interfaces import IAuthentication
from zope.app.security.principalregistry import Principal

def get_auth_data(request):
    return ISession(request)['MyGrokApp.auth']

def get_user(request):
    return get_auth_data(request).get('user', None)

def set_user(request, user):
    get_auth_data(request)['user'] = user

class MyAuthentication(grok.LocalUtility):
    grok.implements(IAuthentication)
    grok.provides(IAuthentication)

    def unauthenticatedPrincipal(self):
        """We implement no unauthenticated principal of our own."""

    def authenticate(self, request):
        user = get_user(request)
        if user:
            title = 'User ' + user
            description = 'The user ' + user
            return Principal(user, title, description, 'aaaaa', 'bbbbb')

You can ignore that aaaaa and bbbbb stuff in that last Principal constructor, or put in something of your own devising. They’re nonsense strings that I added because I never expected to use those last two values that you have to provide with a Principal — they’re called the login and the pw — but if they ever pop up somewhere, hopefully I’ll recognize those strings and know to come back here to set them to something more reasonable!

The Actual CAS Login Part

That previous section of this HOWTO might disappear at any time, because someone wiser will doubtless come along and point out some much easier way of keeping up with the username of who’s logged in; Zope probably has one or more such ways already built in that I just didn’t run across while browsing and in a hurry.

But this next bit is more important, because knowing how to “talk CAS” is the key to what we’re doing here that’s new. Fortunately CAS is a very easy protocol, and a complete implementation of a login page can look exactly like this:

import grok
from urllib import urlopen, urlencode
from mywebapp.app import MyWebApp
from mywebapp.auth import get_user, set_user

class Login(grok.View):
    grok.context(MyWebApp)
    def update(self, redirect=None, ticket=None, logout=None):
        if logout:
            set_user(self.request, None)
        elif ticket:
            data = urlencode({ 'service': self.url(), 'ticket': ticket })
            socket = urlopen('https://your.cas.server/validate', data)
            parts = socket.read().split()
            if parts and parts[0] == 'yes':
                set_user(self.request, parts[1])
                self.redirect(self.url(grok.getSite()))
        elif redirect:
            data = urlencode({ 'service': self.url() })
            self.redirect('https://your.cas.server/login?' + data)
        self.user = get_user(self.request)

This is very straightforward! If the user arrives with the logout parameter set, then we wipe out our knowledge of who they are.

If, instead, they look like they’re returning from having just logged in to CAS and have a ticket purporting to prove their identity to us, then we ask CAS whether it will vouch that the ticket is valid; if so, then we set the current user to the one returned during ticket validation and then redirect the user back to the home page. (This is where one big improvement could take place: if the redirection was back to where the user was before they logged in.)

Else, if they have shown up with redirect set, then this means they’ve asked to go log in to CAS, so we redirect them there.

And if all else fails, we just show them the login page.

“But wait! What does the login page look like?!” I hear you cry. It just so happens that I have it right here (it lives, per the usual Grok conventions, in login_templates/login.pt):

<html>
<body>

<h1>Login Page</h1>

<p tal:condition="view/user">
 You are already logged in.<br>
 Your name is <b tal:content="view/user">username</b>.
 <form tal:attributes="action python:view.url()" method="GET">
  <input type="hidden" name="logout" value="yes" /><br />
  <input type="submit" value="Log out" />
 </form>
</p>
<p tal:condition="not: view/user">
 You are currently <b>not logged in</b>.<br>
 Press the button below to log in using CAS.<br>
 <form tal:attributes="action python:view.url()" method="GET">
  <input type="hidden" name="redirect" value="yes" /><br />
  <input type="submit" value="Log in" />
 </form>
</p>

</body></html>

You will want a prettier one, I have no doubt, but your logic should be the same as that shown here. If the user is not logged in, let them know you’re about to send them somewhere else to check their username and password, and then provide a button that will do it. Otherwise, tell them who they are and offer them a convenient logout button.

Turning On The Authentication

If you install the above, then a login page will appear and users who do log in will successfully get the internal variable set that our auth.py uses to remember their username.

But, that’s not enough for your application to really know who the user is! To accomplish that last step, we need to somehow deliver the user’s username every time the innards of Grok or Zope tries to examine request.principal.id on a request object. (You can usually get to the request from any view through simply asking for view.request.)

And that’s why we created that odd little IAuthentication utility in auth.py. To activate it, you need to import it into your main app.py and then tell your application object to make it active as a local utility:

import grok
from mywebapp.auth import MyAuthentication

...

class MyWebApp(grok.Application, grok.Container):
    grok.local_utility(MyAuthentication)

...

And with that magic in place, you should find that users that you direct to the login page should indeed be able to log in and then be recognized by your app. Good luck.

Note that, because we’ve provided MyAuthentication only as a local utility, the user won’t be authenticated if he moves outside of the URL of your particular Grok app that you’ve done this to!