Mapping TypeKey users to a local store in Catalyst

LTjake on 2006-02-07T15:29:39

The most common usage of Catalyst's Authentication framework, is to map a user to a DBIC/CDBI user in a database (via Store::DBIC) .

At my work, we've created our own TypeKey server for use as a Signle Sign-On point for all of our internal apps. The TypeKey auth. plugin is by default a storeless credential. Meaning once a user if authenticated a basic hash of information sent back from the TypeKey server will be used as the user information for your app.

This is fine, except that you'll have no access to the nice Roles and ACL plugins for Authorization that the framework also provides. So, we should strive to integrate Credential::TypeKey with Store::DBIC somehow.

The process initially sounds simple, except for the fact that when a TypeKey authentication comes your app's way for the first time, the user will not be in your database!

Here's what I had to do to make everything play nice:

  1. Credential/Store config

    I'll use a snippet of my YAML config file as the example:

    authentication:
      typekey:
        server           : http://typekeyserver.com/
        key_url          : http://typekeyserver.com/regkeys.txt
        version          : 1
        auth_store       : default
      dbic:
        user_class        : MyApp::Model::DBIC::User
        user_field        : username
    authorization:
      dbic:
        role_class          : MyApp::Model::DBIC::Role
        role_field          : role
        role_rel            : map_user_role
        user_role_user_field: user
    

    The main points of interest here are the authentication -> typekey -> auth_store and authentication -> dbic sections. By default the TypeKey plugin doesn't use an auth store, but Store::DBIC will set itself as the default store, so we need to make that connection. Also, the dbic section only needs information about the user and nothing about passwords since that's all done remotely on the TypeKey server.

  2. Create our Custom Plugin

    Unfortunately, the Store::DBIC plugin expects a user to exist in the database, so in order to make things work the way we want, we'll have to create our own little plugin that subclasses Store::DBIC.

    Your MyApp.pm's use Catalyst qw(...); section should look something like this:

    use Catalyst qw(
        ...
        Authentication
        Authentication::Credential::TypeKey
        Authentication::Store::DBIC::AutoCreate
        Authorization::ACL
        Authorization::Roles
        ....
    );
    

    Catalyst::Plugin::Authentication::Store::DBIC::AutoCreate doesn't exist anywhere on CPAN, so we'll have to put it in our own lib directory.

    This plugin consists of two files:

  3. AutoCreate.pm

    package Catalyst::Plugin::Authentication::Store::DBIC::AutoCreate;
    
    use strict;
    use warnings;
    
    use base qw( Catalyst::Plugin::Authentication::Store::DBIC );
    
    use Catalyst::Plugin::Authentication::Store::DBIC::AutoCreate::Backend;
    
    sub setup {
        my $c = shift;
    
        $c->NEXT::setup(@_);
        
        $c->default_auth_store(
            Catalyst::Plugin::Authentication::Store::DBIC::AutoCreate::Backend->new( {
                auth  => $c->config->{authentication}->{dbic},
                authz => $c->config->{authorization}->{dbic}
            } )
        );
    }
    
    1;
    

    This will set the default store to an instance of our backend module.

  4. AutoCreate/Backend.pm

    package Catalyst::Plugin::Authentication::Store::DBIC::AutoCreate::Backend;
    
    use strict;
    use warnings;
    
    use base qw( Catalyst::Plugin::Authentication::Store::DBIC::Backend );
    
    sub get_user  {
        my( $self, $username, $verify, $results ) = @_;
    
        my $user = $self->SUPER::get_user( $username );
        
        return $user if $user;
    
        $self->config->{ auth }->{ user_class }->create( $results );
        $user = $self->SUPER::get_user( $username );
        
        return $user;
    }
    
    1;
    

    This will let the superclass attempt to get the user as normal. If it fails, then we need to execute a create then re-fetch the user.

Other than that, your user class will have to do something useful with that data passed to the create sub.

Hopefully someone else will find this mini-tutorial useful. :)