Role-Based Single Sign-on with Perl and Ruby

Single sign-on dictated by user roles with Perl and Ruby.
Front End #1: a Management Application

Ruby on Rails (RoR) shines in the area of database applications that need to provide CRUD (Create, Retrieve, Update, Delete) functionality. It was a simple and easy task to get our database management application up and running (Figure 3). Plenty of good tutorials are available for creating a basic RoR Web application, so in this article I describe only the necessary customizations. As it turned out, there weren't many.

Figure 3. Admin Application, Role Listing

The first thing to note is that I carefully chose the names of the tables and columns to comply with Ruby on Rails naming conventions (Figure 2). This turned out to be a bit tricky; I couldn't find a single source for all the conventions and their implications. In this situation with a join table (admin_roles_admin_users), it was important to concatenate the names in alphabetical order and not to include an id column.

The main customization necessary was to tell RoR about the many-to-many relationship. This was accomplished with a single line added to admin_role.rb:

class AdminRole < ActiveRecord::Base
  has_and_belongs_to_many :admin_users

and an equivalent one in admin_user.rb:

class AdminUser < ActiveRecord::Base
  has_and_belongs_to_many :admin_roles

With these changes, RoR could work with the data correctly and maintain all the proper relationships. In order actually to display and edit the join information, a bit more work was required in the view and controller classes (see Resources). When finished, I had nice screens, such as the one shown in Figure 4.

Figure 4. Admin Application, Editing a Role

With the administrative application in place, we could begin populating the database. But for this information actually to be used, an adapter would have to be written for our Perl/CGI runtime environment.

Front End #2: a Perl/CGI Adapter

I'm a big fan of declarative (as opposed to procedural) programming, when it can be used. What does this mean? Well, one way to check for authorization might look like this:

my $username = $auth->currrent_user;
if (! $username) {
  # Handle the login form
} elsif (! $auth->user_has_role($username, news editor)) {
  # Show error message and exit

Sure, that could be simplified a bit—for example, by implementing a current_user_has_role() method. But it's still procedural, telling the computer what to do. Instead, we can reduce this to one line by telling the computer (declaring) what we want:

$auth->require_role(news editor);

This require_role() method means this role is required to get any further, and it gives a very simple guarantee: execution will proceed beyond this point only if the current user should be able to. If the user 1) already has logged in and 2) has the given role, then require_role() will simply return and the script will continue executing normally. Otherwise, the $auth object will take whatever steps are necessary to first authenticate and then either grant or deny access to users based on their assigned roles.

This makes a lot of things easier. For application programmers, it means they don't have to worry about how the $auth object does its job. Nor do they have to worry about whether they got their ifs and elsifs written correctly. All they need to worry about is what role is appropriate for that script. It was honestly a lot of fun to implement the Perl module and watch so much happen with so little effort required by the application programmer. Figure 5 is a flowchart that shows what happens when require_role is invoked.

Figure 5. Flowchart

Concretely, my implementation required only four short files:

  • the gatekeeper for the system. It implements the business logic of checking first for authentication and second for authorization.

  • login.tt2 (using Template Toolkit): renders a login form with embedded hidden values to keep track of the originally requested destination page. The results of the login attempt are sent to auth_login.cgi.

  • auth_error.tt2: renders an error page, letting users know that they don't have the required authorization to access the script.

  • auth_login.cgi: responsible for the simple task of authenticating the user and restarting the access checking. In our case, it connects to the LDAP system and looks to see if the given login information is correct. If it is, then this fact is saved in a session/cookie, and the originally requested CGI script is re-executed.

Here are the most important sections of each file:

  • The heart of this module is the require_role() method. It contains the control logic for the whole process. In my implementation, I use in the OO style, so I pass it in as a parameter. Notice how the use of return vs. exit controls the user's experience:

    sub require_role {
        # Ensure that the user is logged in and has the 
        # specified role.
        my $self = shift;
        my $role = shift;
        my $cgi  = shift;
        if (! $role) {
               confess("No role was specified");
        if (! $cgi) {
               confess("No CGI object was given");
        my $uname = $self->get_authentication_info();
        if ($uname) {
                # The user has been authenticated.
                if ($self->user_has_role($uname, $role)) {
                        # Success - continue.
                } else {
                        # Failure - the user does not have 
                        # the specified role.
         } else {
    	     # The user has NOT been authenticated.

  • login.tt2: Template Toolkit is an awesome way to create HTML pages. I could have achieved the same thing with a here document in Perl, but this is much cleaner. It also allows the template to be executed from both and auth_login.cgi.

    <p>Please login to access <b>[% target_page %]</b>:</p>	
    <form method="POST" action="/cgi-bin/auth_login.cgi">
        <td>User name:</td><td><input name="username"></td>
       <td>Password:</td><td><input name="password" type="password"></td>
       <td colspan="2" align="right">
        <input type="hidden" name="target_url" value="[% target_url %]">
        <input type="hidden" name="target_page" value="[% target_page %]">
        <input type="submit" value="Login">
    [% IF error_message %]
    <p style="color: #ff0000">
      <b>[% error_message %]</b>
    [% END %]

  • auth_login.cgi: finally, here is the key section from the login form handler. This is a very simple script:

    if (&ldapauth($name, $pass)) {
      # Success: Create a session, and
      # redirect to the target page.
      print "<html><head>";
      print '<meta http-equiv="refresh" content="0;url=' . $target_url . '">';
      print "</head></html>";
    } else {
      # Failure: Re-display the login form with an
      # error message.
      print $q->header;
      &redisplay_page("Login failed: password incorrect.", 

With all the pieces in place, we're ready to go. Here's a simple Perl CGI script that we want to try to protect:

use CGI;
my $q = CGI->new();
print $q->header;

print <<EOF;
<body bgcolor="#ee3333">
  <p align="center" style="color: white">This 
              is a TOP SECRET page.</p>

It creates the output shown in Figure 6. But, now let's modify it to use the new framework:

use CGI;
use Auth;

my $q = CGI->new();

my $a = LC::Auth->new;
$a->require_role( 'top-secret stuff', $q);

print $q->header;
print <<EOF;
<body bgcolor="#ee3333">
  <p align="center" style="color: white">This 
          is a TOP SECRET page.</p>

Figure 6. Unprotected Page

After making this simple change, reloading the browser now shows the same URL, but instead of the top-secret contents, we see a login form (Figure 7). Logging in correctly will do several things in the blink of an eye: send the information to auth_login.cgi, which will verify it, and then store the logged-in state in a session; redirect to the initial page, which will re-execute require_role(), which now finds the session, verifies role membership with the MySQL database; and then returns, allowing the script to display the content. But, as far as users are concerned, after submitting the login form, their application simply appears.

Figure 7. Login Form



Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

What software was used to

Anonymous's picture

What software was used to create the flowcharts?

Which program was used to

Anonymous's picture

Which program was used to create article diagrams and workflows?


Geek Guide
The DevOps Toolbox

Tools and Technologies for Scale and Reliability
by Linux Journal Editor Bill Childers

Get your free copy today

Sponsored by IBM

Upcoming Webinar
8 Signs You're Beyond Cron

Scheduling Crontabs With an Enterprise Scheduler
11am CDT, April 29th
Moderated by Linux Journal Contributor Mike Diehl

Sign up now

Sponsored by Skybot