|
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. We have set you up a session based on AIS response from this server You are not logged in at this server We already have a session for you so we didn't bother this server Something went wrong when we tried to check this AIS
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] |