http://qs321.pair.com?node_id=602005

Introduction

This is a stream of consciousness meditation concerning single sign-on.

The other day, I was clicking around on http://rt.cpan.org/, when I noticed this "Have a Bitcard account? Log in with it now" link on their mainpage. I created a bitcard account and signed in. It seemed pretty slick, so I began to wonder how hard it would be to support. The answer is: not very hard, only seconds required — if you're already acclimated.

Bitcard

I located Authen::Bitcard, which is apparently directly descended from Authen::TypeKey. It turns out it's really fun and easy to support either on your site. I will spare you many of the details, although I was prepared to post a complete example — it seems that I'd want multiple levels of readmore tags before I did that.

This was how I ended up instantiating Authen::Bitcard. The $assigned_bitcard_token is assigned to you by Bjo/rn's site, it's not something you make up. The $bitcard_pubkey_cache is either a filename or a CODE ref — however, if you go that route, be prepared to create Math::BigInts by hand and populate some hashkeys in the $bc object directly. I didn't care for that option and stuck with the file. It still costs you an LWP hit to the site to see if the public key has changed since you last created a $bc object, but it only downloads the key if it has changed.

my $bc = Authen::Bitcard->new; $bc->token($assigned_bitcard_token); $bc->info_optional([qw(name username email)]); $bc->expires( 86400*30 ); $bc->key_cache($bitcard_pubkey_cache);

Next, you simply need to interact with the $bc object to authenticate users. Notice that name, username, and email are all optional? That's one of my favorite parts of these systems. You can authenticate without identifying yourself.

print $cgi->p("Hi there..."); print $cgi->p("Please ", $cgi->a({ href=>$bc->login_url({r=>$cgi->u +rl}) }, "login") . ".");

I have chosen CGI::Session to keep track of my registration data (if available) and my authentication data. I call my session object $ses so it's the same number of characters as $cgi. Here is, pretty much all the logic you'd need to authenticate bidcard users...

$ses->expire("+1y"); # most of the session lasts forever my $dood = $ses->param("dood"); my $user = $ses->param("user"); if( not $user and (my $bcd = $ses->param("bc_data")) ) { $user = $bc->verify( $bcd ); # we only verify the bc_data every h +our or so } if( not $user and (my $f = $cgi->param("bc_fields")) and ($user = $bc- +>verify($cgi)) ) { # This line is stolen from Authen::Bitcard directly (minus a few b +ytes)... # Why don't they export this? my %data = map { $_ => $cgi->param($_) } split(/,/, $f), 'bc_sig'; $ses->param( bc_data => \%data ); # so we can verify above $dood = $user->{username} || $user->{name} || $user->{email} || "Incognito #" . $user->{id}; $ses->param( dood => $dood ); $ses->flush; print $cgi->redirect($cgi->url); exit 0; } if( $cgi->param("lo") or ($user and not $dood) ) { $ses->clear([qw(user bc_data)]); print $cgi->redirect($cgi->url); exit 0; } if( $user and not $ses->param("user") ) { $ses->param( user => $user ); $ses->expire( user => "+1h" ); # this means every hour we'll verif +y(bc_data) }

I was insanely happy with my cute little test app using the logic above. It only took a couple minutes to figure out and it meant I could create sites for which people would not need to create logins. They could just try out my junk and I don't even need to send them an email. (What does sending an email prove anyway?)

Huzzah! I learned about single sign-on. I was reminded vaguely of MS Passport — which nobody used because ... well, duh. But why was I surprised that others were tackling this same problem?

OpenID

Instantaneously, upon showing my friends the coolness of Bitcard, they start whining, "OpenID has already won!!1 Even Digg Supports it OMG!" I don't read digg, but they don't support OpenID. They're about to apparently, but they definitely don't yet. It seems everyone is about to (if they don't already). Just check out my cred at jyte! (That was intended as a joke, but since I have so many thumbs up, who's laughing now!?!)

OpenID is probably better than Bitcard/TypeKey because it's decentralized but it does have it's own problems. First of all, it's somewhat more complicated than Bitcard. Just check out this simple diagram to see what I mean. (It's really not as bad as it looks at first glance.)

The second problem is letting unknown jerks (that's a safe assumption) on the internet tell your LWP::UserAgent where to go. Mr. Fitzpatrick was kind enough to handle the difficulties of that mess for us in his LWPx::ParanoidAgent — if you dare trust it. Personally, I was even more paranoid than the Paranoid Agent in my OpenID tests, but you may be more trusting than me — or are not responsible for the security of dozens of related (or otherwise neighboring) servers and websites.

There are a couple choices for OpenID in Perl. I chose Net::OpenID::Consumer for my testing. In order to get started, I needed to pull in a few other things too though. By the way, it's no coincidence that the documentation for Net::OpenID::Consumer tries to sell you on LWPx::ParanoidAgent; it's the same author!

use Net::OpenID::Consumer; use Digest::SHA1 qw(sha1); use LWPx::ParanoidAgent; use Cache::File; use DBM::Deep; use URI; # for port parsing use Time::HiRes qw(time);

Nonce Sense

First of all, you need a $nonce that jerks can't predict, but that doesn't change during the different stages of a login. You can use a simple secret, but the docs warn you away from this. If you think about it, a million node botnet could probably figure out your secret if it sucks, so it's probably better to shake it up a little.

I chose to use Digest::SHA1, a secret mixed with some weakly random things (aka random key mashing) and the username like so:

my $nonce_pattern = q(%s%d%d%s my secret code words here) . $0; my $nonce = $ses->param("nonce") || sha1(sprintf($nonce_pattern, time, (stat $0)[9], -s _, $clai +m));

(NOTE: I sometimes make things more complicated than they really need to be though. It's make just as much sense to set consumer_secret => sub { sprintf($nonce_pattern, shift) } since the module passes you the session time if you set the secret to a coderef instead.)

Then, after considering the security of the aforementioned Paranoid Agent a little, I chose this setup.

my $ua = LWPx::ParanoidAgent->new; $ua->timeout(5); $ua->blocked_hosts(sub { my $host = shift; return 1 if $host =~ $my_local_networks; # some precompiled regula +r... return 0 if $host =~ m/^[\w\d\-_]+\.myopenid\.com\//; return 0 if $host =~ m/^[\w\d_.-]+\.(?:com|net|org)(?:\/\??[\w\d_& +=;%-]+)?\z/; return 0 if $host =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/ and not $host =~m/^(?:10\.|192\.168|172\.16|127\.)/; return 1; });

I'm not sure it's necessary to match and block 10. and 192.168. as I believe that's default, but you're better safe than sorry. I also restrict the range of identity servers quite a bit (org/net/com). What good is an authenticated identity if I don't know where it's coming from? Maybe just as good as if they were all from http://www.myopenid.com/ I suppose — since it's likely that nefarious individuals could just make as many accounts as they like there. Nevertheless, this way makes me feel a little more comfortable. (I can always relax the rules later.)

Also, using URI, I reject any claimed identity with an $uri->port below 1024 that isn't 80 or 443. I also reject any claimed identity with an @ in the hostname. My thinking being that it'd make using your site as a password cracker proxy more difficult — but, I suspect they'll try anyway. I also reject any $uri->path =~ m(\.\./).

It doesn't appear that you can reject based on the $uri with LWPx::ParanoidAgent directly, but you certainly can filter the $claim and then pass $uri->canonical to the Agent; which, is precisely what I did.

Anyway, now we have enough pieces to construct our OpenID Consumer. The required_root here is the path under which our return(s) to the site must fall.

my $csr = Net::OpenID::Consumer->new( consumer_secret => $nonce, args => $cgi, ua => $ua, required_root => $this_domain, cache => Cache::File->new(cache_root=>"$session_director +y/csr.cache/"));

OpenID Authentication Logic

First things first, we send the user to the claimed URI along with our $nonce (crammed in an HMAC-SHA1 apparently). Although I know little about the actual protocol of OpenID, this will let us verify the returned auth msg later. The trusted_root here is the domain and path you're asking the user to trust. Some OpenID servers will ask the user to confirm it in some way. I'm not completely clear on the details, but the important part is that it's the URL you're asking them to trust ... it can actually contain globs and things.

if ( $claim and not $cgi->param("checked") ) { if( my $ci = $csr->claimed_identity($claim) ) { my $check_url = $ci->check_url( return_to => $this_domain . "myscript.pl?claim=$claim;che +cked=1", trust_root => $this_domain, ); $ses->param( nonce => $nonce ); # keep this for when they come + back print $cgi->redirect( $check_url ); exit 0; } }

If the user is not already logged in and registered at your site, they'll get re-redirected back to you for further url-ification. In my setup, I respectfully request any information they're willing to share with me. There doesn't seem to be an interface in Net::OpenID::Consumer for this, but it's simple enough to add. This data request is known as simple registration.

elsif( my $setup_url = $csr->user_setup_url ) { # We only get here if we're not already logged into myopenid... print $cgi->redirect( $setup_url . '&openid.sreg.optional=' . 'email,nickname,fullname,dob,gender,postcode,country,languag +e,timezone' ); exit 0; }

Once they log in and release their data to you, they'll get re-re-redirected back to you, along with any simple registration data — iff they haven't already released that data to you. In the case of http://www.myopenid.com/, you can [allow once], [allow forever] and [deny] the login/registration process. If you allow forever, the sreg data will only be released the first time. However, each time you allow once it'll re-release the sreg data. Is that behavior standard? I certainly don't know, but I use DBM::Deep to grab the sreg data while I can.

elsif( (my $vfid = $csr->verified_identity) or (my $user = $ses->param +("user")) ) { my $dbm = DBM::Deep->new(file=>$dbm_file, locking => 1, autoflush +=> 1); if( $vfid ) { my $url = $vfid->url; my $max = 20; for my $p ( grep {m/^openid\.sreg\./} $cgi->param ) { if( $p =~ m/\.([^.]+)\z/ ) { my $k = $1; my $v = substr $cgi->param($p), 0, 1024; warn "adding $k=$v to $url"; $dbm->{$url}{$k} = $v; } last if (--$max)<1; } $user = { url => $url, disp => $dbm->{$url}->{nickname} || $dbm->{$url}->{fullnam +e} || $vfid->display, time => time, }; $ses->param(user=>$user); $ses->flush; } elsif( $cgi->param("lo") ) { $ses->clear([qw(user nonce)]); # kill the nonce so they get a +new one next time $ses->flush; print $cgi->redirect( $cgi->url ); exit 0; } my $url = $user->{url}; print $ses->header; print $cgi->start_html({title=>"openid test"}); print $cgi->h3("Hi there $user->{disp}!"); print $cgi->p("We've come a long way I think, no?", $cgi->a({href= +>"?lo=1"}, "logout")); print $cgi->p("reg params:", $cgi->ul( map {$cgi->li("$_: " . $dbm->{$url}{$_}) } keys %{ $ +dbm->{$url} } )) if %{ $dbm->{$url} } exit 0; }

Concluding Thoughts

OpenID is a lot more complicated, but it has "already won" the single sign-on, um, contest. Apparently it's everywhere already and, in truth, if you look around you'll see it all over the place. I can't help but wonder what other choices there are. Bitcard is pretty neat and much simpler to implement, but I do see the inherent value of decentralizing the identity servers.

The next site I make that requires a login is going to use one of these methods for sure (probably OpenID). There's really no point in emailing a user and designing the login logic when stuff like this is available.

What other methods are out there? What are you using? ... and most importantly, what did I miss above?

-Paul

Replies are listed 'Best First'.
Re: Concerning Single Sign-on, Bitcard (TypeKey), and OpenID, CACERT client certificate
by varian (Chaplain) on Feb 25, 2007 at 16:44 UTC
    An alternative solution could be to deploy client and server certificates. All well known web browsers support client certificates and the authentication process can take place without any enduser involvement. The client certificate can be used against any number of webservers which makes it a single signon mechanism. Certificates are digitally signed to a root authority

    Some references:

    • Read here to learn more about these public key certificates
    • CACERT is a root authority organization that delivers client- and server certificates worldwide at no charge
    • Apache webservers have built in support for certificates and certificate based authentication. To finetune it to your needs you can optionally write a small (mod)perl program that uses the ID information provided by Apache to decide if the user will be entitled access
    Like with most single signon technologies the registration process is the most challenging part as it involves the enduser to interact, submit passphrases etc, and to accept/install their client certificate.
    Now this is probably true for any single signon mechanism that does not rely on a physical token. Once the client certificate is installed it works like a charm.

      I don't wish to advocate OpenID exactly (although I do think it's neat). I actually like Bitcard better, but nobody besides rt.cpan uses it as far as I know.

      ...most single signon technologies the registration process is the most challenging part...

      How hard would it be to switch identities and change which info you share with each site? I don't think CA/PKI is really set up for that kind of identity management. But the real problem is, I can't see my mom signing up for and maintaining a keyring of x509 signatures — dealing with keys and dealing with the expirations — but I can see her using things like OpenID.

      There's a video of a guy setting up a myopenid account in 8 seconds. He then uses it to log into a wiki.

      The CACERT stuff will never actually be included in MSIE, but it's a nice idea. One way it blows OpenID out of the water is that if your identity server goes down, you can't log into anything; which obviously isn't true with CA/PKI (aside from revocations).

      Someone just pointed out to me that it's almost worse than I just said. If you use the OpenID authentication delegation so you can use your own URL as your identity, then if either site goes down you can't log into anything.

      -Paul

        There are people that would vote for centralized authentication management and others that favor decentralized systems. To speak for myself I am a little wary of the centralized approach.

        The single point of failure that you mentioned yourselves is one reason. If the central authentication server fails then all dependent servers have become inaccessable as well.

        The second, even more compelling, reason is that if the central system gets compromised then the intruder can find ways to freely obtain id's and subsequently can access any server that relies on the central authentication process. This means that the central authentication service must be protected by extreme measures to provide some level of protection against intrusion.

        The third reason is one of trust and accountability. Would you trust the access to your server to a third party controlled authentication process? Now if the third party is under heavy government supervision one might be inclined to accept the risk, but otherwise...

        This is why I like public key certificates. It is a decentralized authentication procedure, server and browser only interact with each other. Still the same client certificate can be used to present an ID for as many servers as one likes. The ID carries a digital signature of both the user and a co-signing central root authority which relates to level of trustworthiness. In principle the enduser would need to manage only one client certificate.

        I don't wish to advocate OpenID exactly (although I do think it's neat). I actually like Bitcard better, but nobody besides rt.cpan uses it as far as I know.

        The NTP Pool and CPAN Ratings are other notable users.

        I'm planning to add OpenID support to Bitcard, too. The big advantage/difference with Bitcard is that the email addresses you get from Bitcard will be verified etc already, so you can do away with logic to do that in your application. If you just need an identifier that doesn't change then OpenID is all you need.

        - ask

        ask bjoern hansen, http://www.askbjoernhansen.com/
Re: Concerning Single Sign-on, Bitcard (TypeKey), and OpenID
by zentara (Archbishop) on Feb 25, 2007 at 17:42 UTC
    <brainstorming> It would seem logical and efficient to me, to have certificates on a USB key(or swipe card). A universal program to do this would be a great step toward banking security, and online-voting systems. Better yet, subcutaneous(spelling fixed :-) ) implants. :-) The Borg are coming....... the Borg are coming...... </brainstorming>

    I'm not really a human, but I play one on earth. Cogito ergo sum a bum
      The Borg certainly make the registration process easier to comply with and resistance is futile anyway ;-)
Re: Concerning Single Sign-on, Bitcard (TypeKey), and OpenID (Complete OpenID Example)
by eric256 (Parson) on Jan 07, 2008 at 16:12 UTC

    I had a heck of a time figuring this all out, so here is a complete working example. FYI if you are installing Net::OpenID::Consumer it will want to install Crypt::DH, this will take a very long time (several hours) unless you install Math::BigInt:GMP. I couldn't find this anywhere except the bug reports, and it took a while to find which module was causing CPAN to hang (i'm no CPAN guru). I hope this helps out someone else.

    #!/usr/bin/perl use strict; use warnings; use Net::OpenID::Consumer; use LWPx::ParanoidAgent; use Digest::SHA1 qw(sha1); use CGI; use CGI::Session; use Data::Dumper; my $cgi = new CGI; my $session = new CGI::Session() or die CGI::Session->errstr; $session->expire('+1h'); my $domain = "http://www.movablecircus.com"; my $login_url = $domain . "/openid.pl"; my $home_url = $domain . "/openid.pl"; my $openid = $cgi->param("openid_url"); my $nonce_pattern = q(%s%d%d%s my secret code words here) . $0; my $nonce = $session->param("nonce") || sha1(sprintf($nonce_pattern, time, (stat $0)[9], -s _, $session +->id)); $session->param("nonce", $nonce); if ($cgi->param("logout")) { $session->delete(); print $cgi->redirect($login_url); exit 0; } my $csr = Net::OpenID::Consumer->new( ua => LWPx::ParanoidAgent->new, args => $cgi, consumer_secret => $nonce, required_root => $domain, ); if ($openid) { # a user entered, say, "bradfitz.com" as their identity. The firs +t # step is to fetch that page, parse it, and get a # Net::OpenID::ClaimedIdentity object: my $checked = $cgi->param("checked"); if (!$checked) { # we arn't returning from a check, so send out the check my $claimed_identity = $csr->claimed_identity($cgi->param("ope +nid_url")); if ($claimed_identity) { my $check_url = $claimed_identity->check_url( return_to => $login_url . "?checked=1;openid_url=$ope +nid", trust_root => $domain, ); print $cgi->redirect($check_url); exit; } } elsif( my $setup_url = $csr->user_setup_url ) { # We only get here if we're not already logged into myopenid.. +. print $cgi->redirect( $setup_url . '&openid.sreg.optional=' . 'email,nickname,fullname' ); exit 0; } elsif( (my $vfid = $csr->verified_identity) ) { print $cgi->redirect($home_url); $session->param("user", $cgi->param("openid.identity")); exit 0; } } elsif (not $session->param("user") ) { # user not logged in yet print $session->header(); print "<html><body>\n"; print <<HTML; <form method="GET" style="margin: 0"> <input type="text" name="openid_url" id="openid_url" value="" +size="30" style="margin-bottom: 0" /> <input type="submit" value="Sign in" style="margin: 0"/> <br /><small>e.g. http://username.myopenid.com</small> </form> HTML } else { # user logged in print $session->header(); print "<html><body>\n"; print "<p>Welcome " . $session->param("user") . "</p>"; print "<a href='$login_url?logout=1'>Logout</a>"; print "</body>"; }

    ___________
    Eric Hodges