|
| From: | david nicol |
| Subject: | [dgAIS] Talk at YAPC/America 2002 |
| Date: | Mon, 01 Jul 2002 23:03:15 -0500 |
To do:
Add support for storing and retrieving form data that might arrive
before a session is created, to the demonstration AIS client
Translate the client and server into The Language Of Your Choice
--
"It is a method of my own: crude but adequate"
-- Professor Henry Jarrod|
an authenticated identity service david nicol YAPC/America 2002 Herein is described a working multiple-step protocol for sharing a single sign-on identity among a family of web services, with working sample implementation of both server and client parts. Features include:
Some definitions: web server: a *BSD box running Boa, or equivalent cookie: see http://www.netscape.com/newsref/std/cookie_spec.html web service: an infrastructure-level feature of the developing "world wide web" AIS: Authenticated Identity Service User: an entity, and their machinery, wanting to use a web service that uses AIS AIS server: a web server providing this AIS service AIS server resource identifier (AISSRI) : a character string to which AIS server request types can be postpended to obtain AIS services, such as "http://www.pay2send.com/cgi/AIS/" SSO: single sign-on. Establish the user's identity to the satisfaction of the AIS server. Session: maintained by a particular service, or AIS client. Time and state diagrams There are three participants in the operation of AIS: the user, the AIS client, and the AIS server. The states described in the state columns of the table below indicate the state at the completion of the step described in that row. Step zero, in which the user demonstrates their identity to the AIS server, can be replaced by any other single-sign-on method, such as web server infrastructures that request log-in credentials and provide a HTTP_USER variable in the CGI environment.
In response to a request for a restricted resource made outside of a session, the AIS client web service directs user to request a single-use identification key.
User requests single-use identification key
AIS generates key and directs user back to web service
web service presents AIS with key
AIS provides user identity to web service
at this point, the web service can create its own session object and cookie the user with a key associating them with it. The AIS client can use the key the AIS server produced, or a new key -- this is between the AIS client and it's users only. Content-Type: text/plain <?xml version="1.0" encoding="ISO-8859-1"?> <aisresponse> <identity>address@hidden</identity> <aissri>http://www.pay2send.com/cgi/ais/</aissri> <user_remote_addr>65.26.97.7</user_remote_addr> </aisresponse>"NULL" is reserved as the identity provided when there was no key. "ERROR" is reserved as an identity to indicate that there is a problem requiring intervention, such as the AIS client's owner has not paid their dues to whoever is hosting this particular AIS service. Implementations may reserve other identities, or extend AIS-XML to include more data. implementing an AIS server Assuming your AIS server is already part of a SSO domain of some kind, the AIS server can be implemented with only two functions, "present" to trigger generation of a new key and "query" to offer authenticated identities in exchange for keys. The following example uses dbmopen to store all types of data persistently in a single database. A revised suite of AIS server programs using three databases may be available from the web page by the time you read this. #!/usr/local/bin/perl -I.
=pod
this is a sample AIS "present" program, which
reads a SSO session certificate from a cookie
called AIS_Session and uses DirDB
for data persistence.
If you are installing this somewhere where you
already have a HTTP_USER environment variable,
just use that instead if you want, to determine
what user is presenting; but you still need to
store the mapping back from the single-use key
somewhere.
=cut
# -d 'data' or mkdir 'data', 0777 or die "could not create data directory";
# use Fcntl ':flock'; # import LOCK_* constants
# open LOCK,'>>data/AIS_lock' or die 'Cannot open Lock File';
# flock LOCK, LOCK_EX;
# dbmopen(%DATA,'data/AIS_data',0660); # so much easier to write than "tie..."
use DirDB;
tie %Sessions, 'DirDB', 'data/Sessions';
tie %OTU_keys, 'DirDB', 'data/OTU_keys';
# Determine Identity from Cookies:
my $Ses_key;
$ENV{HTTP_COOKIE} =~ m/AIS_Session=(\w+)/ and $Ses_key = $1;
=pod
The single-sign-on keys are set elsewhere; the AIS client
is responsible for directing NULL users to a log-in page
or something like that -- logging people in is not the
"present" script's problem
=cut
my $Identity = $Ses_key ? $Sessions{$Ses_key} : 'NULL';
my $Single_Use_Key = join('',time,(map {("A".."Z")[rand 26]} (0..19)), $$);
# remember identity under the single-use-key:
$OTU_keys{$Single_Use_Key} = <<IDENTITYBLOCK;
<identity>$Identity</identity>
<aissri>http://$ENV{SERVER_NAME}/cgi/ais/</aissri>
<user_remote_addr>$ENV{REMOTE_ADDR}</user_remote_addr>
IDENTITYBLOCK
# send our user back to the AIS client
print "Location: $ENV{QUERY_STRING}$Single_Use_Key\n\n";
# every twenty present runs, clean up OTU keys older than two minutes
unless ($$ % 20){
close STDOUT;
delete @OTU_keys{grep {(time - $_) > 120 } keys %OTU_keys};
};
exit;
__END__
The previous program, "present," creates single-use keys and maps identities to them. To get the identity back, a program called "query" will provide the identity mapped to a single-use key. #!/usr/local/bin/perl
=pod
this is a sample AIS "query" program, which
looks up single-use keys provided in its query string and
replies with a block of AIS-XML.
=cut
use DirDB;
tie %OTU, 'DirDB', './data/OTU_keys';
my $xmlblock = $OTU{$ENV{QUERY_STRING}} || <<DEFAULT;
<identity>ERROR</identity>
<error>provided single use key not found in AIS data</error>
<aissri>http://$ENV{SERVER_NAME}/cgi/ais/</aissri>
<user_remote_addr>$ENV{REMOTE_ADDR}</user_remote_addr>
DEFAULT
delete $OTU{$ENV{QUERY_STRING}}; # anyone know how to
# incorporate return-by-delete
# into the previous statement?
print <<EOF and exit;
Content-Type: text/plain
<?xml version="1.0" encoding="ISO-8859-1"?>
<aisresponse>
$xmlblock</aisresponse>
EOF
__END__
With the above two programs in place, it is possible to issue a request
for, say, http://pay2send.com/cgi/ais/present?http://pay2send.com/cgi/ais/query?
and get an XML page.
To have this system be good for anything requires a single-sign-on realm of some kind, and then some AIS client software. I present below
the ais/add program looks for a CGI variable "email" and creates a single-sign-on identity mapping for the e-mail address given, and e-mails the mapping code to the address. Without such a variable, it displays a log-in page.
#!/usr/local/bin/perl -I.
=pod
this is a sample AIS "add" program, which
will add a sign-on identity to its database or
display a log-in page if no "email" variable appears
in the CGI data.
=cut
# look for "email" in CGI data
$rawdata = join '', $ENV{QUERY_STRING}, <STDIN>;
($email) = ($rawdata =~ m/email=([^&]+)/);
$email =~ s/%(..)/chr(hex($1))/ge;
# look for an e-mail address
($email) = ($email =~ m/([^\s\<\>address@hidden|address@hidden<\>address@hidden|]+)/);
unless ($email){
print <<EOF;
Content-Type: text/html
<title>AIS log-in page</title>
<body bgcolor=ffffff>
<form method=POST action="">
What is a good e-mail address for you?
<input type=text name="email">
<input type=submit value="send me a log-in key">
</form>
</body>
EOF
exit;
};
use DirDB; # a concurrent-write-safe database :)
tie %DATA,'DirDB','data/SSO_keys';
my $SSO_key = join '',time,(map {("A".."Z")[rand 26]} (0..15)), $$;
$DATA{$SSO_key} = $email;
open(MAIL,"|sendmail -t -i -f 'address@hidden'");
print MAIL <<EOF;
To: <$email>
From: address@hidden
X-Abuse-To: (tracert $ENV{HTTP_ADDR})address@hidden
Subject: AIS LOGIN LINKS for $email
Content-Type: text/html
<body bgcolor=ffffff>
<form method=POST action="">
<input type=hidden name="SK" value="$SSO_key">
To log your web browser into the single sign-on service
at $ENV{SERVER_NAME}/cgi/ais/,
<input type=submit value="click here">
<p>
To log your web browser out, click here:<p>
<a href="">
http://$ENV{SERVER_NAME}/cgi/ais/logout
</a>
<p>
To disable the log-in button in this message click here:<p>
<a href="">
http://$ENV{SERVER_NAME}/cgi/ais/delete?$SSO_key
</a>
<p>
You appear to have requested this log-in link while using
a $ENV{HTTP_USER_AGENT} web browser from IP address $ENV{REMOTE_ADDR}
<p>
</body>
EOF
print <<EOF;
Content-Type: text/html
<body bgcolor=ffffff>
You are connecting from $ENV{REMOTE_ADDR}<p>
A message containing an AIS log-in button has been e-mailed to <$email>
</body>
EOF
__END__
To be an AIS client, a web service needs to have permission to access the AIS server. Some AIS servers, such as the sample ones here, are public, but others may have subscription or membership access models.
package CGI::AIS::Session;
use strict;
use vars qw{ *SOCK @ISA @EXPORT $VERSION };
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(Authenticate);
$VERSION = '0.01';
use Carp;
use Socket qw(:DEFAULT :crlf);
use IO::Handle;
sub miniget($$$$){
my($HostName, $PortNumber, $Desired, $agent) = @_;
$PortNumber ||= 80;
# print STDERR ~~localtime,"Trying to connect to $HostName $PortNumber to retrieve $Desired\n";
my $iaddr = inet_aton($HostName) || die "Cannot find host named $HostName";
my $paddr = sockaddr_in($PortNumber,$iaddr);
my $proto = getprotobyname('tcp');
socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
connect(SOCK, $paddr) || die "connect: $!";
SOCK->autoflush(1);
print SOCK
"GET $Desired HTTP/1.1$CRLF",
# Do we need a Host: header with an "AbsoluteURI?"
# not needed: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.2
# but this is trumped by an Apache error message invoking RFC2068 sections 9 and 14.23
"Host: $HostName$CRLF",
"User-Agent: $agent$CRLF",
"Connection: close$CRLF",
$CRLF;
join('',<SOCK>);
};
sub Authenticate{
my %Param = (agent => 'AISclient', @_);
my %Result;
my $AISXML;
# print STDERR "cookie string is $ENV{HTTP_COOKIE}\n";
my ($Cookie) = ($ENV{HTTP_COOKIE} =~ /AIS_Session=(\w+)/);
tie my %Session, $Param{tieargs}->[0],
$Param{tieargs}->[1],$Param{tieargs}->[2],$Param{tieargs}->[3],
$Param{tieargs}->[4],$Param{tieargs}->[5],$Param{tieargs}->[6],
$Param{tieargs}->[7],$Param{tieargs}->[8],$Param{tieargs}->[9]
or croak "failed to tie @{$Param{tieargs}}";
if ($Cookie and ! $Session{$Cookie}){
$Cookie = '';
};
my $OTUkey;
my $SessionKey;
if ($ENV{QUERY_STRING} =~ /AIS_OTUkey=(\w+)/){
$OTUkey = $1;
my ($method, $host, $port, $path) =
($Param{aissri} =~ m#^(\w+)://([^:/]+):?(\d*)(.+)$#)
or die "Could not get meth,hos,por,pat from <$Param{aissri}>";
unless ($method eq 'http'){
croak "aissri parameter must begin 'http://' at this time";
};
# print STDERR "about to miniget for: ${CRLF}GET $Param{aissri}query?$OTUkey$CRLF$CRLF";
# my $Response = `lynx -source $Param{aissri}query?$OTUkey$CRLF$CRLF`
my $Response = miniget $host, $port,
"$Param{aissri}query?$OTUkey", $Param{agent};
$SessionKey = join('',time,(map {("A".."Z")[rand 26]}(0..19)));
print "Set-Cookie: AIS_Session=$SessionKey;$CRLF";
($AISXML) =
$Response =~ m#<aisresponse>(.+)#si
or die "no <aisresponse> element from $Param{aissri}query?$OTUkey\n";
$Session{$SessionKey} = $AISXML;
}elsif (!$Cookie){
print "Location: $Param{aissri}present?http://$ENV{SERVER_NAME}$ENV{REQUEST_URI}?AIS_OTUkey=\n\n";
exit;
}else{ # We have a cookie
$AISXML = $Session{$Cookie};
delete $Session{$Cookie} if $ENV{QUERY_STRING} eq 'AIS_LOGOUT';
};
foreach (qw{
identity
error
aissri
user_remote_addr
},
@{$Param{XML}}
){
# print STDERR "Looking for $_ in XML\n";
$AISXML =~ m#<$_>(.+)$_>#si or next;
$Result{$_} = $1;
# print STDERR "Found $Result{$_}\n";
};
if ( defined($Param{timeout})){
my $TO = $Param{timeout};
delete @Session{ grep { time - $_ > $TO } keys %Session };
};
#Suppress caching NULL and ERROR
if( $Result{identity} eq 'NULL' or $Result{identity} eq 'ERROR'){
print "Set-Cookie: AIS_Session=$CRLF";
$SessionKey and delete $Session{$SessionKey} ;
};
# print STDERR "About to return session object\n";
# print STDERR "@{[%Result]}\n";
return \%Result;
};
# Preloaded methods go here.
1;
__END__
=head1 NAME
CGI::AIS::Session - Perl extension to manage CGI user sessions with external identity authentication via AIS
=head1 SYNOPSIS
use DirDB; # or any other concurrent-access-safe
# persistent hash abstraction
use CGI::AIS::Session;
my $Session = Authenticate(
aissri <= 'http://www.pay2send.com/cgi/ais/',
tieargs <= ['DirDB', './data/Sessions'],
XML <= ['name','age','region','gender'],
agent <= 'Bollow', # this is the password for the AIS service, if needed
( $$ % 100 ? () : (timeout <= 4 * 3600)) # four hours
);
if($$Session{identity} eq 'NULL'){
print "Location: http://www.pay2send.com/cgi/ais/login\n\n"
exit;
}elsif($Session->{identity} eq 'ERROR'){
print "Content-type: text/plain\n\n";
print "There was an error with the authentication layer",
" of this web service: $Session->{error}\n\n",
"please contact $ENV{SERVER_ADMIN} to report this.";
exit;
}
tie my %UserData, 'DirDB', "./data/$$Session{identity}";
=head1 DESCRIPTION
Creates and maintains a read-only session abstraction based on data in
a central AIS server.
The session data provided by AIS is read-only. A second
database keyed on the identity provided by AIS should be
used to store persistent local information such as shopping cart
contents. This may be repaired in future releases, so the
session object will be more similar to the session objects
used with the Apache::Session modules, but for now, all the
data in the object returned by C<Authenticate> comes from the
central AIS server.
On the first use, the user is redirected to the AIS server
according to the AIS protocol. Then the identity, if any,
is cached
under a session key in the session database as tied to by
the 'tieargs' parameter.
This module will create a http cookie named AIS_Session.
Authenticate will croak on aissri methods other than
http in this version.
Additional expected XML fields can be listed in an XML parameter.
If a 'timeout' paramter is provided, Sessions older than
the timeout get deleted from the tied sessions hash.
'ERROR' and 'NULL' identities are not cached.
Internally, the possible states of this system are:
no cookie, no OTU
OTU
cookie
Only the last one results in returning a session object. The
other two cause redirection.
if a query string of AIS_LOGOUT is postpended to any url in the
domain protected by this module, the session will be deleted before
it times out.
=head1 EXPORTS
the Authenticate routine is exported.
=head1 AUTHOR
David Nicol, address@hidden
=head1 SEE ALSO
http://www.pay2send.com/ais/ais.html
The Apache::Session family of modules on CPAN
=cut
#!/usr/bin/perl -Iguarded
use DirDB; # or any other concurrent-access-safe
# persistence abstraction
use CGI::AIS::Session;
my $Session = Authenticate(
aissri => 'http://www.pay2send.com/cgi/ais/',
agent => "guarding: $ENV{SERVER_NAME}$ENV{SCRIPT_NAME}",
tieargs => ['DirDB', './guarded/Sessions'],
( $$ % 100 ? () : (timeout => 4 * 3600)) # four hours
);
if($$Session{identity} eq 'NULL'){
print "Location: http://www.pay2send.com/cgi/ais/add\n\n";
exit;
}elsif($Session->{identity} eq 'ERROR'){
print <<EOF;
Content-type: text/plain
There was an error with the authentication layer
of this web service: $Session->{error}
please contact $ENV{SERVER_ADMIN} to report this.
EOF
exit;
}
print <<EOF;
Content-type: text/plain
we have session $Session
@{[keys %$Session]}
@{[values %$Session]}
Path-info is $ENV{PATH_INFO}
EOF
if (-e "guarded/text/$ENV{PATH_INFO}"){
open TEXT,"<guarded/text/$ENV{PATH_INFO}";
while (<TEXT>){
print
};
print "\n\nTo log out, access http://$ENV{SERVER_NAME}$ENV{SCRIPT_NAME}?AIS_LOGOUT\n";
}else{
print "no such document found.\n";
}
__END__
The example programs here use a trivial flat file database with key names as file names. It is available on CPAN as "DirDB." The way multiple AIS embedding would work is like this: instead of offering a log-in page that refers to a single identity authority, the "splash page" of the guarded area has multiple images embedded in it, each a hyperlink to its respective login page. When the user tries to display the image, an AIS client handshake occurs, and a "logged in" or "not logged in" image will be displayed, either directly by a program, or through redirection to a static image. "Click here to log in" makes more sense than "not logged in." This AIS client feature is under development.
Which server utility uses which kinds of persistent data?
The AIS handshake as a time-state chart
In early drafts (Yes, something this simple does goes through more than one draft) of this protocol, the one-time-use keys were generated by the AIS client. This approach would be vulnerable to key collision problems, and also assymetric network visibility problems, and that is why I think that the additional step is justified: the client can be simpler. webservice subscriber identification method Providers of AIS Services that wish to limit access to their databases, for privacy or subscription-based business model, are encouraged to embed a password in the user-agent header provided in the QUERY request, which is supported by the miniget routine in the example module as well
as by LWP. Limiting access to queries based on REMOTE_ADDR would work as
well, but it is kind of hacky to rely on a transport-layer implementation detail
which is subject to change to implement a higher-level feature.
Each installation of an AIS server should provide some information about its intended purpose and privacy and subscription policies and so on at a standard place, ${aissri}about. One way to provide this is to have an "about" program that prints out your about page. Another is to have an "about" program that prints a redirection header that sends the user to the static about page, such as
If you would like to help develop this project, please join the discussion list at http://savannah.gnu.org/projects/tjais/ |
| [Prev in Thread] | Current Thread | [Next in Thread] |