root/trunk/extlib/Net/OpenID/Consumer.pm @ 3531

Revision 3531, 44.4 kB (checked in by fumiakiy, 9 months ago)

Merged sockfish to trunk. "svn merge -r3114:3527 http://code.sixapart.com/svn/movabletype/branches/sockfish/ ."

Line 
1# LICENSE: You're free to distribute this under the same terms as Perl itself.
2
3use strict;
4use Carp ();
5use LWP::UserAgent;
6use Storable;
7
8############################################################################
9package Net::OpenID::Consumer;
10
11use vars qw($VERSION);
12$VERSION = "1.03";
13
14use fields (
15    'cache',           # a Cache object to store HTTP responses and associations
16    'ua',              # LWP::UserAgent instance to use
17    'args',            # how to get at your args
18    'message',         # args interpreted as an IndirectMessage, if possible
19    'consumer_secret', # scalar/subref
20    'required_root',   # the default required_root value, or undef
21    'last_errcode',    # last error code we got
22    'last_errtext',    # last error code we got
23    'debug',           # debug flag or codeblock
24    'minimum_version', # The minimum protocol version to support
25);
26
27use Net::OpenID::ClaimedIdentity;
28use Net::OpenID::VerifiedIdentity;
29use Net::OpenID::Association;
30use Net::OpenID::Yadis;
31use Net::OpenID::IndirectMessage;
32use Net::OpenID::URIFetch;
33
34use MIME::Base64 ();
35use Digest::SHA1 ();
36use Crypt::DH 0.05;
37use Time::Local;
38use HTTP::Request;
39
40sub new {
41    my Net::OpenID::Consumer $self = shift;
42    $self = fields::new( $self ) unless ref $self;
43    my %opts = @_;
44
45    $opts{minimum_version} ||= 1;
46
47    $self->{ua}            = delete $opts{ua};
48    $self->args            ( delete $opts{args}            );
49    $self->cache           ( delete $opts{cache}           );
50    $self->consumer_secret ( delete $opts{consumer_secret} );
51    $self->required_root   ( delete $opts{required_root}   );
52    $self->minimum_version ( delete $opts{minimum_version} );
53
54    $self->{debug} = delete $opts{debug};
55
56    Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
57    return $self;
58}
59
60# NOTE: This method is here only to support the openid-test library.
61# Don't call it from anywhere else, or you'll break when it gets
62# removed. Instead, set the minimum_version property.
63# FIXME: Can we just make openid-test set minimum_version and get
64# rid of this?
65sub disable_version_1 {
66    my $self = shift;
67    $self->{minimum_version} = 2.0;
68}
69
70sub cache           { &_getset; }
71sub consumer_secret { &_getset; }
72sub required_root   { &_getset; }
73sub minimum_version { &_getset; }
74
75sub _getset {
76    my Net::OpenID::Consumer $self = shift;
77    my $param = (caller(1))[3];
78    $param =~ s/.+:://;
79
80    if (@_) {
81        my $val = shift;
82        Carp::croak("Too many parameters") if @_;
83        $self->{$param} = $val;
84    }
85    return $self->{$param};
86}
87
88sub _debug {
89    my Net::OpenID::Consumer $self = shift;
90    return unless $self->{debug};
91
92    if (ref $self->{debug} eq "CODE") {
93        $self->{debug}->($_[0]);
94    } else {
95        print STDERR "[DEBUG Net::OpenID::Consumer] $_[0]\n";
96    }
97}
98
99# given something that can have GET arguments, returns a subref to get them:
100#   Apache
101#   Apache::Request
102#   CGI
103#   HASH of get args
104#   CODE returning get arg, given key
105
106#   ...
107
108sub args {
109    my Net::OpenID::Consumer $self = shift;
110
111    if (my $what = shift) {
112        unless (ref $what) {
113            return $self->{args} ? $self->{args}->($what) : Carp::croak("No args defined");
114        }
115        else {
116            Carp::croak("Too many parameters") if @_;
117            my $message = Net::OpenID::IndirectMessage->new($what, (
118                minimum_version => $self->minimum_version,
119            ));
120            $self->{message} = $message;
121            $self->{args} = $message ? $message->getter : sub { undef };
122        }
123    }
124    $self->{args};
125}
126
127sub message {
128    my Net::OpenID::Consumer $self = shift;
129    if (my $key = shift) {
130        return $self->{message} ? $self->{message}->get($key) : undef;
131    }
132    else {
133        return $self->{message};
134    }
135}
136
137sub _message_mode {
138    my $message = $_[0]->message;
139    return $message ? $message->mode : undef;
140}
141
142sub _message_version {
143    my $message = $_[0]->message;
144    return $message ? $message->protocol_version : undef;
145}
146
147sub ua {
148    my Net::OpenID::Consumer $self = shift;
149    $self->{ua} = shift if @_;
150    Carp::croak("Too many parameters") if @_;
151
152    # make default one on first access
153    unless ($self->{ua}) {
154        my $ua = $self->{ua} = LWP::UserAgent->new;
155        $ua->timeout(10);
156    }
157
158    $self->{ua};
159}
160
161sub _fail {
162    my Net::OpenID::Consumer $self = shift;
163    my ($code, $text) = @_;
164
165    $text ||= {
166        'no_identity_server' => "The provided URL doesn't declare its OpenID identity server.",
167        'empty_url' => "No URL entered.",
168        'bogus_url' => "Invalid URL.",
169        'no_head_tag' => "URL provided doesn't seem to have a head tag.",
170        'url_fetch_err' => "Error fetching the provided URL.",
171        'bad_mode' => "The openid.mode argument is not correct",
172        'protocol_version_incorrect' => "The provided URL uses the wrong protocol version",
173        'naive_verify_failed_return' => "Provider says signature is invalid",
174        'naive_verify_failed_network' => "Could not contact provider to verify signature",
175    }->{$code};
176
177    $self->{last_errcode} = $code;
178    $self->{last_errtext} = $text;
179
180    $self->_debug("fail($code) $text");
181    wantarray ? () : undef;
182}
183
184sub json_err {
185    my Net::OpenID::Consumer $self = shift;
186    return OpenID::util::js_dumper({
187        err_code => $self->{last_errcode},
188        err_text => $self->{last_errtext},
189    });
190}
191
192sub err {
193    my Net::OpenID::Consumer $self = shift;
194    $self->{last_errcode} . ": " . $self->{last_errtext};
195}
196
197sub errcode {
198    my Net::OpenID::Consumer $self = shift;
199    $self->{last_errcode};
200}
201
202sub errtext {
203    my Net::OpenID::Consumer $self = shift;
204    $self->{last_errtext};
205}
206
207sub _get_url_contents {
208    my Net::OpenID::Consumer $self = shift;
209    my ($url, $final_url_ref, $hook) = @_;
210    $final_url_ref ||= do { my $dummy; \$dummy; };
211
212    my $res = Net::OpenID::URIFetch->fetch($url, $self, $hook);
213
214    $$final_url_ref = $res->final_uri;
215
216    return $res ? $res->content : undef;
217}
218
219sub _find_semantic_info {
220    my Net::OpenID::Consumer $self = shift;
221    my $url = shift;
222    my $final_url_ref = shift;
223
224    my $trim_hook = sub {
225        my $htmlref = shift;
226        # trim everything past the body.  this is in case the user doesn't
227        # have a head document and somebody was able to inject their own
228        # head.  -- brad choate
229        $$htmlref =~ s/<body\b.*//is;
230    };
231
232    my $doc = $self->_get_url_contents($url, $final_url_ref, $trim_hook) || '';
233
234    # find <head> content of document (notably: the first head, if an attacker
235    # has added others somehow)
236    return $self->_fail("no_head_tag", "Couldn't find OpenID servers due to no head tag")
237        unless $doc =~ m!<head[^>]*>(.*?)</head>!is;
238    my $head = $1;
239
240    my $ret = {
241        'openid.server' => undef,
242        'openid.delegate' => undef,
243        'foaf' => undef,
244        'foaf.maker' => undef,
245        'rss' => undef,
246        'atom' => undef,
247    };
248
249    # analyze link/meta tags
250    while ($head =~ m!<(link|meta)\b([^>]+)>!g) {
251        my ($type, $val) = ($1, $2);
252        my $temp;
253
254        # OpenID servers / delegated identities
255        # <link rel="openid.server" href="http://www.livejournal.com/misc/openid.bml" />
256        if ($type eq "link" &&
257            $val =~ /\brel=.openid\.(server|delegate)./i && ($temp = $1) &&
258            $val =~ m!\bhref=[\"\']([^\"\']+)[\"\']!i) {
259            $ret->{"openid.$temp"} = $1;
260            next;
261        }
262
263        # OpenID2 providers / local identifiers
264        # <link rel="openid2.provider" href="http://www.livejournal.com/misc/openid.bml" />
265        if ($type eq "link" &&
266            $val =~ /\brel=.openid2\.(provider|local_id)./i && ($temp = $1) &&
267            $val =~ m!\bhref=[\"\']([^\"\']+)[\"\']!i) {
268            $ret->{"openid2.$temp"} = $1;
269            next;
270        }
271
272        # FOAF documents
273        #<link rel="meta" type="application/rdf+xml" title="FOAF" href="http://brad.livejournal.com/data/foaf" />
274        if ($type eq "link" &&
275            $val =~ m!title=.foaf.!i &&
276            $val =~ m!rel=.meta.!i &&
277            $val =~ m!type=.application/rdf\+xml.!i &&
278            $val =~ m!href=[\"\']([^\"\']+)[\"\']!i) {
279            $ret->{"foaf"} = $1;
280            next;
281        }
282
283        # FOAF maker info
284        # <meta name="foaf:maker" content="foaf:mbox_sha1sum '4caa1d6f6203d21705a00a7aca86203e82a9cf7a'" />
285        if ($type eq "meta" &&
286            $val =~ m!name=.foaf:maker.!i &&
287            $val =~ m!content=([\'\"])(.*?)\1!i) {
288            $ret->{"foaf.maker"} = $2;
289            next;
290        }
291
292        if ($type eq "meta" &&
293            $val =~ m!name=.foaf:maker.!i &&
294            $val =~ m!content=([\'\"])(.*?)\1!i) {
295            $ret->{"foaf.maker"} = $2;
296            next;
297        }
298
299        # RSS
300        # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.livejournal.com/~brad/data/rss" />
301        if ($type eq "link" &&
302            $val =~ m!rel=.alternate.!i &&
303            $val =~ m!type=.application/rss\+xml.!i &&
304            $val =~ m!href=[\"\']([^\"\']+)[\"\']!i) {
305            $ret->{"rss"} = $1;
306            next;
307        }
308
309        # Atom
310        # <link rel="alternate" type="application/atom+xml" title="Atom" href="http://www.livejournal.com/~brad/data/rss" />
311        if ($type eq "link" &&
312            $val =~ m!rel=.alternate.!i &&
313            $val =~ m!type=.application/atom\+xml.!i &&
314            $val =~ m!href=[\"\']([^\"\']+)[\"\']!i) {
315            $ret->{"atom"} = $1;
316            next;
317        }
318    }
319
320    # map the 4 entities that the spec asks for
321    my $emap = {
322        'lt' => '<',
323        'gt' => '>',
324        'quot' => '"',
325        'amp' => '&',
326    };
327    foreach my $k (keys %$ret) {
328        next unless $ret->{$k};
329        $ret->{$k} =~ s/&(\w+);/$emap->{$1} || ""/eg;
330    }
331
332    $self->_debug("semantic info ($url) = " . join(", ", map { $_.' => '.$ret->{$_} } keys %$ret)) if $self->{debug};
333
334    return $ret;
335}
336
337sub _find_openid_server {
338    my Net::OpenID::Consumer $self = shift;
339    my $url = shift;
340    my $final_url_ref = shift;
341
342    my $sem_info = $self->_find_semantic_info($url, $final_url_ref) or
343        return;
344
345    return $self->_fail("no_identity_server") unless $sem_info->{"openid.server"};
346    $sem_info->{"openid.server"};
347}
348
349sub is_server_response {
350    my Net::OpenID::Consumer $self = shift;
351    return $self->_message_mode ? 1 : 0;
352}
353
354sub handle_server_response {
355    my Net::OpenID::Consumer $self = shift;
356    my %callbacks_in = @_;
357    my %callbacks = ();
358
359    foreach my $cb (qw(not_openid setup_required cancelled verified error)) {
360        $callbacks{$cb} = delete($callbacks_in{$cb}) || sub { Carp::croak("No ".$cb." callback") };
361    }
362    Carp::croak("Unknown callbacks ".join(',', keys %callbacks)) if %callbacks_in;
363
364    unless ($self->is_server_response) {
365        return $callbacks{not_openid}->();
366    }
367
368    if (my $setup_url = $self->user_setup_url) {
369        return $callbacks{setup_required}->($setup_url);
370    }
371    elsif ($self->user_cancel) {
372        return $callbacks{cancelled}->();
373    }
374    elsif (my $vident = $self->verified_identity) {
375        return $callbacks{verified}->($vident);
376    }
377    else {
378        return $callbacks{error}->($self->errcode, $self->errtext);
379    }
380
381}
382
383sub _discover_acceptable_endpoints {
384    my Net::OpenID::Consumer $self = shift;
385    my $url = shift;
386    my %opts = @_;
387
388    # if return_early is set, we'll return as soon as we have enough
389    # information to determine the "primary" endpoint, and return
390    # that as the first (and possibly only) item in our response.
391    my $primary_only = delete $opts{primary_only} ? 1 : 0;
392
393    my $force_version = delete $opts{force_version};
394
395    Carp::croak("Unknown option(s) ".join(', ', keys(%opts))) if %opts;
396
397    # trim whitespace
398    $url =~ s/^\s+//;
399    $url =~ s/\s+$//;
400    return $self->_fail("empty_url", "Empty URL") unless $url;
401
402    # do basic canonicalization
403    $url = "http://$url" if $url && $url !~ m!^\w+://!;
404    return $self->_fail("bogus_url", "Invalid URL") unless $url =~ m!^https?://!i;
405    # add a slash, if none exists
406    $url .= "/" unless $url =~ m!^https?://.+/!i;
407
408    my @discovered_endpoints = ();
409    my $result = sub {
410        # We always prefer 2.0 endpoints to 1.1 ones, regardless of
411        # the priority chosen by the identifier.
412        return [
413            (grep { $_->{version} == 2 } @discovered_endpoints),
414            (grep { $_->{version} == 1 } @discovered_endpoints),
415        ];
416    };
417
418    # TODO: Support XRI too?
419
420    # First we Yadis service discovery
421    my $yadis = Net::OpenID::Yadis->new(consumer => $self);
422    if ($yadis->discover($url)) {
423        # FIXME: Currently we don't ever do _find_semantic_info in the Yadis
424        # code path, so an extra redundant HTTP request is done later
425        # when the semantic info is accessed.
426
427        my $final_url = $yadis->identity_url;
428        my @services = $yadis->services(
429            OpenID::util::version_2_xrds_service_url(),
430            OpenID::util::version_2_xrds_directed_service_url(),
431            OpenID::util::version_1_xrds_service_url(),
432        );
433        my $version2 = OpenID::util::version_2_xrds_service_url();
434        my $version1 = OpenID::util::version_1_xrds_service_url();
435        my $version2_directed = OpenID::util::version_2_xrds_directed_service_url();
436
437        foreach my $service (@services) {
438            my $service_uris = $service->URI;
439
440            # Service->URI seems to return all sorts of bizarre things, so let's
441            # normalize it to always be an arrayref.
442            if (ref($service_uris) eq 'ARRAY') {
443                my @sorted_id_servers = sort {
444                    my $pa = $a->{priority};
445                    my $pb = $b->{priority};
446                    return 0 unless defined($pa) || defined($pb);
447                    return -1 unless defined ($pb);
448                    return 1 unless defined ($pa);
449                    return $a->{priority} <=> $b->{priority}
450                } @$service_uris;
451                $service_uris = \@sorted_id_servers;
452            }
453            if (ref($service_uris) eq 'HASH') {
454                $service_uris = [ $service_uris->{content} ];
455            }
456            unless (ref($service_uris)) {
457                $service_uris = [ $service_uris ];
458            }
459
460            my $delegate = undef;
461            my @versions = ();
462
463            if (grep(/^${version2}$/, $service->Type)) {
464                # We have an OpenID 2.0 end-user identifier
465                $delegate = $service->extra_field("LocalID");
466                push @versions, 2;
467            }
468            if (grep(/^${version1}$/, $service->Type)) {
469                # We have an OpenID 1.1 end-user identifier
470                $delegate = $service->extra_field("Delegate", "http://openid.net/xmlns/1.0");
471                push @versions, 1;
472            }
473
474            if (@versions) {
475                foreach my $version (@versions) {
476                    next if defined($force_version) && $force_version != $version;
477                    foreach my $uri (@$service_uris) {
478                        push @discovered_endpoints, {
479                            uri => $uri,
480                            version => $version,
481                            final_url => $final_url,
482                            delegate => $delegate,
483                            sem_info => undef,
484                            mechanism => "Yadis",
485                        };
486                    }
487                }
488            }
489
490            if (grep(/^${version2_directed}$/, $service->Type)) {
491                # We have an OpenID 2.0 OP identifier (i.e. we're doing directed identity)
492                my $version = 2;
493                # In this case, the user's claimed identifier is a magic value
494                # and the actual identifier will be determined by the provider.
495                my $final_url = OpenID::util::version_2_identifier_select_url();
496                my $delegate = OpenID::util::version_2_identifier_select_url();
497
498                foreach my $uri (@$service_uris) {
499                    push @discovered_endpoints, {
500                        uri => $uri,
501                        version => $version,
502                        final_url => $final_url,
503                        delegate => $delegate,
504                        sem_info => undef,
505                        mechanism => "Yadis",
506                    };
507                }
508            }
509
510            if ($primary_only && scalar(@discovered_endpoints)) {
511                # We've got at least one endpoint now, so return early
512                return $result->();
513            }
514        }
515    }
516
517    # Now HTML-based discovery, both 2.0- and 1.1-style.
518    {
519        my $final_url = undef;
520        my $sem_info = $self->_find_semantic_info($url, \$final_url);
521
522        if ($sem_info) {
523            if ($sem_info->{"openid2.provider"}) {
524                unless (defined($force_version) && $force_version != 2) {
525                    push @discovered_endpoints, {
526                        uri => $sem_info->{"openid2.provider"},
527                        version => 2,
528                        final_url => $final_url,
529                        delegate => $sem_info->{"openid2.local_id"},
530                        sem_info => $sem_info,
531                        mechanism => "HTML",
532                    };
533                }
534            }
535            if ($sem_info->{"openid.server"}) {
536                unless (defined($force_version) && $force_version != 1) {
537                    push @discovered_endpoints, {
538                        uri => $sem_info->{"openid.server"},
539                        version => 1,
540                        final_url => $final_url,
541                        delegate => $sem_info->{"openid.delegate"},
542                        sem_info => $sem_info,
543                        mechanism => "HTML",
544                    };
545                }
546            }
547        }
548    }
549
550    return $result->();
551
552}
553
554# returns Net::OpenID::ClaimedIdentity
555sub claimed_identity {
556    my Net::OpenID::Consumer $self = shift;
557    my $url = shift;
558    Carp::croak("Too many parameters") if @_;
559
560    # trim whitespace
561    $url =~ s/^\s+//;
562    $url =~ s/\s+$//;
563    return $self->_fail("empty_url", "Empty URL") unless $url;
564
565    # do basic canonicalization
566    $url = "http://$url" if $url && $url !~ m!^\w+://!;
567    return $self->_fail("bogus_url", "Invalid URL") unless $url =~ m!^https?://!i;
568    # add a slash, if none exists
569    $url .= "/" unless $url =~ m!^https?://.+/!i;
570
571    my $endpoints = $self->_discover_acceptable_endpoints($url, primary_only => 1);
572
573    if (ref($endpoints) && @$endpoints) {
574        foreach my $endpoint (@$endpoints) {
575
576            next unless $endpoint->{version} >= $self->minimum_version;
577
578            $self->_debug("Discovered version $endpoint->{version} endpoint at $endpoint->{uri} via $endpoint->{mechanism}");
579            $self->_debug("Delegate is $endpoint->{delegate}") if $endpoint->{delegate};
580
581            return Net::OpenID::ClaimedIdentity->new(
582                identity         => $endpoint->{final_url},
583                server           => $endpoint->{uri},
584                consumer         => $self,
585                delegate         => $endpoint->{delegate},
586                protocol_version => $endpoint->{version},
587                semantic_info    => $endpoint->{sem_info},
588            );
589
590        }
591
592        # If we've fallen out here, then none of the available services are of the required version.
593        return $self->_fail("protocol_version_incorrect");
594
595    }
596    else {
597        return $self->_fail("no_identity_server");
598    }
599
600}
601
602sub user_cancel {
603    my Net::OpenID::Consumer $self = shift;
604    return $self->_message_mode eq "cancel";
605}
606
607sub user_setup_url {
608    my Net::OpenID::Consumer $self = shift;
609    my %opts = @_;
610    my $post_grant = delete $opts{'post_grant'};
611    Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
612
613    if ($self->_message_version == 1) {
614        return $self->_fail("bad_mode") unless $self->_message_mode eq "id_res";
615    }
616    else {
617        return undef unless $self->_message_mode eq 'setup_needed';
618    }
619
620    my $setup_url = $self->message("user_setup_url");
621
622    OpenID::util::push_url_arg(\$setup_url, "openid.post_grant", $post_grant)
623        if $setup_url && $post_grant;
624
625    return $setup_url;
626}
627
628sub verified_identity {
629    my Net::OpenID::Consumer $self = shift;
630    my %opts = @_;
631
632    my $rr = delete $opts{'required_root'} || $self->{required_root};
633    Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
634
635    return $self->_fail("bad_mode") unless $self->_message_mode eq "id_res";
636
637    # the asserted identity (the delegated one, if there is one, since the protocol
638    # knows nothing of the original URL)
639    my $a_ident  = $self->message("identity")     or return $self->_fail("no_identity");
640
641    my $sig64    = $self->message("sig")          or return $self->_fail("no_sig");
642
643    # fix sig if the OpenID auth server failed to properly escape pluses (+) in the sig
644    $sig64 =~ s/ /+/g;
645
646    my $returnto = $self->message("return_to")    or return $self->_fail("no_return_to");
647    my $signed   = $self->message("signed");
648
649    my $possible_endpoints;
650    my $server;
651    my $claimed_identity;
652
653    my $real_ident;
654    if ($self->_message_version == 1) {
655        $real_ident = $self->args("oic.identity") || $a_ident;
656
657        # In version 1, we have to assume that the primary server
658        # found during discovery is the one sending us this message.
659        $possible_endpoints = $self->_discover_acceptable_endpoints($real_ident, force_version => 1);
660
661        if ($possible_endpoints && @$possible_endpoints) {
662            $possible_endpoints = [ $possible_endpoints->[0] ];
663            $server = $possible_endpoints->[0]{uri};
664        }
665        else {
666            # We just fall out of here and bail out below for having no endpoints.
667        }
668    }
669    else {
670        $real_ident = $self->message("claimed_id") || $a_ident;
671
672        # In version 2, the OP tells us its URL.
673        $server = $self->message("op_endpoint");
674        $possible_endpoints = $self->_discover_acceptable_endpoints($real_ident, force_version => 2);
675
676        # FIXME: It kinda sucks that the above will always do both Yadis and HTML discovery, even though
677        # in most cases only one will be in use.
678    }
679
680    $self->_debug("Server is $server");
681
682    unless ($possible_endpoints && @$possible_endpoints) {
683        return $self->_fail("no_identity_server");
684    }
685
686    # check that returnto is for the right host
687    return $self->_fail("bogus_return_to") if $rr && $returnto !~ /^\Q$rr\E/;
688
689    # check age/signature of return_to
690    my $now = time();
691    {
692        my ($sig_time, $sig) = split(/\-/, $self->args("oic.time") || "");
693        # complain if more than an hour since we sent them off
694        return $self->_fail("time_expired")   if $sig_time < $now - 3600;
695        # also complain if the signature is from the future by more than 30 seconds,
696        # which compensates for potential clock drift between nodes in a web farm.
697        return $self->_fail("time_in_future") if $sig_time - 30 > $now;
698        # and check that the time isn't faked
699        my $c_secret = $self->_get_consumer_secret($sig_time);
700        my $good_sig = substr(OpenID::util::hmac_sha1_hex($sig_time, $c_secret), 0, 20);
701        return $self->_fail("time_bad_sig") unless $sig eq $good_sig;
702    }
703
704    my $last_error = undef;
705
706    foreach my $endpoint (@$possible_endpoints) {
707        my $final_url = $endpoint->{final_url};
708        my $endpoint_uri = $endpoint->{uri};
709        my $delegate = $endpoint->{delegate};
710
711        my $error = sub {
712            $self->_debug("$endpoint_uri not acceptable: ".$_[0]);
713            $last_error = $_[0];
714        };
715
716        # The endpoint_uri must match our $server
717        if ($endpoint_uri ne $server) {
718            $error->("server_not_allowed");
719            next;
720        }
721
722        # OpenID 2.0 wants us to exclude the fragment part of the URL when doing equality checks
723        my $a_ident_nofragment = $a_ident;
724        my $real_ident_nofragment = $real_ident;
725        my $final_url_nofragment = $final_url;
726        if ($self->_message_version >= 2) {
727            $a_ident_nofragment =~ s/\#.*$//x;
728            $real_ident_nofragment =~ s/\#.*$//x;
729            $final_url_nofragment =~ s/\#.*$//x;
730        }
731        unless ($final_url_nofragment eq $real_ident_nofragment) {
732            $error->("unexpected_url_redirect");
733            next;
734        }
735
736        # Protocol version must match
737        unless ($endpoint->{version} == $self->_message_version) {
738            $error->("protocol_version_incorrect");
739            next;
740        }
741
742        # if openid.delegate was used, check that it was done correctly
743        if ($a_ident_nofragment ne $real_ident_nofragment) {
744            unless ($delegate eq $a_ident_nofragment) {
745                $error->("bogus_delegation");
746                next;
747            }
748        }
749
750        # If we've got this far then we've found the right endpoint.
751
752        $claimed_identity =  Net::OpenID::ClaimedIdentity->new(
753            identity         => $endpoint->{final_url},
754            server           => $endpoint->{uri},
755            consumer         => $self,
756            delegate         => $endpoint->{delegate},
757            protocol_version => $endpoint->{version},
758            semantic_info    => $endpoint->{sem_info},
759        );
760        last;
761
762    }
763
764    unless ($claimed_identity) {
765        # We failed to find a good endpoint in the above loop, so
766        # lets bail out.
767        return $self->_fail($last_error);
768    }
769
770    my $assoc_handle = $self->message("assoc_handle");
771
772    $self->_debug("verified_identity: assoc_handle: $assoc_handle");
773    my $assoc = Net::OpenID::Association::handle_assoc($self, $server, $assoc_handle);
774
775    my %signed_fields;   # key (without openid.) -> value
776
777    # Auth 2.0 requires certain keys to be signed.
778    if ($self->_message_version >= 2) {
779        my %signed_fields = map {$_ => 1} split /,/, $signed;
780        my %unsigned_fields;
781        # these fields must be signed unconditionally
782        foreach my $f (qw/op_endpoint return_to response_nonce assoc_handle/) {
783            $unsigned_fields{$f}++ if !$signed_fields{$f};
784        }
785        # these fields must be signed if present
786        foreach my $f (qw/claimed_id identity/) {
787            next unless $self->args("openid.$f");
788            $unsigned_fields{$f}++ if !$signed_fields{$f};
789        }
790        if (%unsigned_fields) {
791            return $self->_fail(
792                "unsigned_field",
793                "Field(s) must be signed: " . join(", ", keys %unsigned_fields)
794            );
795        }
796    }
797
798    if ($assoc) {
799        $self->_debug("verified_identity: verifying with found association");
800
801        return $self->_fail("expired_association")
802            if $assoc->expired;
803
804        # verify the token
805        my $token = "";
806        foreach my $param (split(/,/, $signed)) {
807            my $val = $self->args("openid.$param");
808            $token .= "$param:$val\n";
809            $signed_fields{$param} = $val;
810        }
811
812        my $good_sig = OpenID::util::b64(OpenID::util::hmac_sha1($token, $assoc->secret));
813        return $self->_fail("signature_mismatch") unless $sig64 eq $good_sig;
814
815    } else {
816        $self->_debug("verified_identity: verifying using HTTP (dumb mode)");
817        # didn't find an association.  have to do dumb consumer mode
818        # and check it with a POST
819        my %post = (
820                    "openid.mode"         => "check_authentication",
821                    "openid.assoc_handle" => $assoc_handle,
822                    "openid.signed"       => $signed,
823                    "openid.sig"          => $sig64,
824                    );
825
826        # and copy in all signed parameters that we don't already have into %post
827        foreach my $param (split(/,/, $signed)) {
828            next unless $param =~ /^[\w\.]+$/;
829            my $val = $self->args('openid.'.$param);
830            $signed_fields{$param} = $val;
831            next if $post{"openid.$param"};
832            $post{"openid.$param"} = $val;
833        }
834
835        # if the server told us our handle as bogus, let's ask in our
836        # check_authentication mode whether that's true
837        if (my $ih = $self->message("invalidate_handle")) {
838            $post{"openid.invalidate_handle"} = $ih;
839        }
840
841        my $req = HTTP::Request->new(POST => $server);
842        $req->header("Content-Type" => "application/x-www-form-urlencoded");
843        $req->content(join("&", map { "$_=" . OpenID::util::eurl($post{$_}) } keys %post));
844
845        my $ua  = $self->ua;
846        my $res = $ua->request($req);
847
848        # uh, some failure, let's go into dumb mode?
849        return $self->_fail("naive_verify_failed_network") unless $res && $res->is_success;
850
851        my $content = $res->content;
852        my %args = OpenID::util::parse_keyvalue($content);
853
854        # delete the handle from our cache
855        if (my $ih = $args{'invalidate_handle'}) {
856            Net::OpenID::Association::invalidate_handle($self, $server, $ih);
857        }
858
859        return $self->_fail("naive_verify_failed_return") unless
860            $args{'is_valid'} eq "true" ||  # protocol 1.1
861            $args{'lifetime'} > 0;          # DEPRECATED protocol 1.0
862    }
863
864    $self->_debug("verified identity! = $real_ident");
865
866    # verified!
867    return Net::OpenID::VerifiedIdentity->new(
868        claimed_identity => $claimed_identity,
869        consumer  => $self,
870        signed_fields => \%signed_fields,
871    );
872}
873
874sub supports_consumer_secret { 1; }
875
876sub _get_consumer_secret {
877    my Net::OpenID::Consumer $self = shift;
878    my $time = shift;
879
880    my $ss;
881    if (ref $self->{consumer_secret} eq "CODE") {
882        $ss = $self->{consumer_secret};
883    } elsif ($self->{consumer_secret}) {
884        $ss = sub { return $self->{consumer_secret}; };
885    } else {
886        Carp::croak("You haven't defined a consumer_secret value or subref.\n");
887    }
888
889    my $sec = $ss->($time);
890    Carp::croak("Consumer secret too long") if length($sec) > 255;
891    return $sec;
892}
893
894package OpenID::util;
895
896use constant VERSION_1_NAMESPACE => "http://openid.net/signon/1.1";
897use constant VERSION_2_NAMESPACE => "http://specs.openid.net/auth/2.0";
898
899# I guess this is a bit daft since constants are subs anyway,
900# but whatever.
901sub version_1_namespace {
902    return VERSION_1_NAMESPACE;
903}
904sub version_2_namespace {
905    return VERSION_2_NAMESPACE;
906}
907sub version_1_xrds_service_url {
908    return VERSION_1_NAMESPACE;
909}
910sub version_2_xrds_service_url {
911    return "http://specs.openid.net/auth/2.0/signon";
912}
913sub version_2_xrds_directed_service_url {
914    return "http://specs.openid.net/auth/2.0/server";
915}
916sub version_2_identifier_select_url {
917    return "http://specs.openid.net/auth/2.0/identifier_select";
918}
919
920# From Digest::HMAC
921sub hmac_sha1_hex {
922    unpack("H*", &hmac_sha1);
923}
924sub hmac_sha1 {
925    hmac($_[0], $_[1], \&Digest::SHA1::sha1, 64);
926}
927sub hmac {
928    my($data, $key, $hash_func, $block_size) = @_;
929    $block_size ||= 64;
930    $key = &$hash_func($key) if length($key) > $block_size;
931
932    my $k_ipad = $key ^ (chr(0x36) x $block_size);
933    my $k_opad = $key ^ (chr(0x5c) x $block_size);
934
935    &$hash_func($k_opad, &$hash_func($k_ipad, $data));
936}
937
938sub parse_keyvalue {
939    my $reply = shift;
940    my %ret;
941    $reply =~ s/\r//g;
942    foreach (split /\n/, $reply) {
943        next unless /^(\S+?):(.*)/;
944        $ret{$1} = $2;
945    }
946    return %ret;
947}
948
949sub ejs
950{
951    my $a = $_[0];
952    $a =~ s/[\"\'\\]/\\$&/g;
953    $a =~ s/\r?\n/\\n/gs;
954    $a =~ s/\r//;
955    return $a;
956}
957
958# Data::Dumper for JavaScript
959sub js_dumper {
960    my $obj = shift;
961    if (ref $obj eq "HASH") {
962        my $ret = "{";
963        foreach my $k (keys %$obj) {
964            $ret .= "$k: " . js_dumper($obj->{$k}) . ",";
965        }
966        chop $ret;
967        $ret .= "}";
968        return $ret;
969    } elsif (ref $obj eq "ARRAY") {
970        my $ret = "[" . join(", ", map { js_dumper($_) } @$obj) . "]";
971        return $ret;
972    } else {
973        return $obj if $obj =~ /^\d+$/;
974        return "\"" . ejs($obj) . "\"";
975    }
976}
977
978sub eurl
979{
980    my $a = $_[0];
981    $a =~ s/([^a-zA-Z0-9_\,\-.\/\\\: ])/uc sprintf("%%%02x",ord($1))/eg;
982    $a =~ tr/ /+/;
983    return $a;
984}
985
986sub push_url_arg {
987    my $uref = shift;
988    $$uref =~ s/[&?]$//;
989    my $got_qmark = ($$uref =~ /\?/);
990
991    while (@_) {
992        my $key = shift;
993        my $value = shift;
994        $$uref .= $got_qmark ? "&" : ($got_qmark = 1, "?");
995        $$uref .= eurl($key) . "=" . eurl($value);
996    }
997}
998
999sub push_openid2_url_arg {
1000    my $uref = shift;
1001    my %args = @_;
1002    push_url_arg($uref,
1003        'openid.ns' => VERSION_2_NAMESPACE,
1004        map {
1005            'openid.'.$_ => $args{$_}
1006        } keys %args,
1007    );
1008}
1009
1010sub time_to_w3c {
1011    my $time = shift || time();
1012    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($time);
1013    $mon++;
1014    $year += 1900;
1015
1016    return sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ",
1017                   $year, $mon, $mday,
1018                   $hour, $min, $sec);
1019}
1020
1021sub w3c_to_time {
1022    my $hms = shift;
1023    return 0 unless
1024        $hms =~ /^(\d{4,4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/;
1025
1026    my $time;
1027    eval {
1028        $time = Time::Local::timegm($6, $5, $4, $3, $2 - 1, $1);
1029    };
1030    return 0 if $@;
1031    return $time;
1032}
1033
1034sub bi2bytes {
1035    my $bigint = shift;
1036    die "Can't deal with negative numbers" if $bigint->is_negative;
1037
1038    my $bits = $bigint->as_bin;
1039    die unless $bits =~ s/^0b//;
1040
1041    # prepend zeros to round to byte boundary, or to unset high bit
1042    my $prepend = (8 - length($bits) % 8) || ($bits =~ /^1/ ? 8 : 0);
1043    $bits = ("0" x $prepend) . $bits if $prepend;
1044
1045    return pack("B*", $bits);
1046}
1047
1048sub bi2arg {
1049    return b64(bi2bytes($_[0]));
1050}
1051
1052sub b64 {
1053    my $val = MIME::Base64::encode_base64($_[0]);
1054    $val =~ s/\s+//g;
1055    return $val;
1056}
1057
1058sub d64 {
1059    return MIME::Base64::decode_base64($_[0]);
1060}
1061
1062sub bytes2bi {
1063    return Math::BigInt->new("0b" . unpack("B*", $_[0]));
1064}
1065
1066sub arg2bi {
1067    return undef unless defined $_[0] and $_[0] ne "";
1068    # don't acccept base-64 encoded numbers over 700 bytes.  which means
1069    # those over 4200 bits.
1070    return Math::BigInt->new("0") if length($_[0]) > 700;
1071    return bytes2bi(MIME::Base64::decode_base64($_[0]));
1072}
1073
1074
1075__END__
1076
1077=head1 NAME
1078
1079Net::OpenID::Consumer - library for consumers of OpenID identities
1080
1081=head1 SYNOPSIS
1082
1083  use Net::OpenID::Consumer;
1084
1085  my $csr = Net::OpenID::Consumer->new(
1086    ua    => LWPx::ParanoidAgent->new,
1087    cache => Some::Cache->new,
1088    args  => $cgi,
1089    consumer_secret => ...,
1090    required_root => "http://site.example.com/",
1091  );
1092
1093  # a user entered, say, "bradfitz.com" as their identity.  The first
1094  # step is to fetch that page, parse it, and get a
1095  # Net::OpenID::ClaimedIdentity object:
1096
1097  my $claimed_identity = $csr->claimed_identity("bradfitz.com");
1098
1099  # now your app has to send them at their identity server's endpoint
1100  # to get redirected to either a positive assertion that they own
1101  # that identity, or where they need to go to login/setup trust/etc.
1102
1103  my $check_url = $claimed_identity->check_url(
1104    return_to  => "http://example.com/openid-check.app?yourarg=val",
1105    trust_root => "http://example.com/",
1106  );
1107
1108  # so you send the user off there, and then they come back to
1109  # openid-check.app, then you see what the identity server said.
1110
1111  # Either use callback-based API (recommended)...
1112  $csr->handle_server_response(
1113      not_openid => sub {
1114          die "Not an OpenID message";
1115      },
1116      setup_required => sub {
1117          my $setup_url = shift;
1118          # Redirect the user to $setup_url
1119      },
1120      cancelled => sub {
1121          # Do something appropriate when the user hits "cancel" at the OP
1122      },
1123      verified => sub {
1124          my $vident = shift;
1125          # Do something with the VerifiedIdentity object $vident
1126      },
1127      error => sub {
1128          my $err = shift;
1129          die($err);
1130      },
1131  );
1132
1133  # ... or handle the various cases yourself
1134  if (my $setup_url = $csr->user_setup_url) {
1135       # redirect/link/popup user to $setup_url
1136  } elsif ($csr->user_cancel) {
1137       # restore web app state to prior to check_url
1138  } elsif (my $vident = $csr->verified_identity) {
1139       my $verified_url = $vident->url;
1140       print "You are $verified_url !";
1141  } else {
1142       die "Error validating identity: " . $csr->err;
1143  }
1144
1145
1146=head1 DESCRIPTION
1147
1148This is the Perl API for (the consumer half of) OpenID, a distributed
1149identity system based on proving you own a URL, which is then your
1150identity.  More information is available at:
1151
1152  http://openid.net/
1153
1154=head1 CONSTRUCTOR
1155
1156=over 4
1157
1158=item C<new>
1159
1160my $csr = Net::OpenID::Consumer->new([ %opts ]);
1161
1162You can set the C<ua>, C<cache>, C<consumer_secret>, C<required_root>,
1163C<minimum_version> and C<args> in the constructor.  See the corresponding
1164method descriptions below.
1165
1166=back
1167
1168=head1 METHODS
1169
1170=over 4
1171
1172=item $csr->B<ua>($user_agent)
1173
1174=item $csr->B<ua>
1175
1176Getter/setter for the LWP::UserAgent (or subclass) instance which will
1177be used when web donwloads are needed.  It's highly recommended that
1178you use LWPx::ParanoidAgent, or at least read its documentation so
1179you're aware of why you should care.
1180
1181=item $csr->B<cache>($cache)
1182
1183=item $csr->B<cache>
1184
1185Getter/setter for the optional (but recommended!) cache instance you
1186want to use for storing fetched parts of pages.  (identity server
1187public keys, and the E<lt>headE<gt> section of user's HTML pages)
1188
1189The $cache object can be anything that has a -E<gt>get($key) and
1190-E<gt>set($key,$value) methods.  See L<URI::Fetch> for more
1191information.  This cache object is just passed to L<URI::Fetch>
1192directly.
1193
1194=item $nos->B<consumer_secret>($scalar)
1195
1196=item $nos->B<consumer_secret>($code)
1197
1198=item $code = $nos->B<consumer_secret>; ($secret) = $code->($time);
1199
1200The consumer secret is used to generate self-signed nonces for the
1201return_to URL, to prevent spoofing.
1202
1203In the simplest (and least secure) form, you configure a static secret
1204value with a scalar.  If you use this method and change the scalar
1205value, any outstanding requests from the last 30 seconds or so will fail.
1206
1207The more robust (but more complicated) form is to supply a subref that
1208returns a secret based on the provided I<$time>, a unix timestamp.
1209And if one doesn't exist for that time, create, store and return it
1210(with appropriate locking so you never return different secrets for
1211the same time.)
1212
1213Your secret may not exceed 255 characters.
1214
1215=item $csr->B<minimum_version>(2)
1216
1217=item $csr->B<minimum_version>
1218
1219Get or set the minimum OpenID protocol version supported. Currently
1220the only useful value you can set here is 2, which will cause
12211.1 identifiers to fail discovery with the error C<protocol_version_incorrect>.
1222
1223In most cases you'll want to allow both 1.1 and 2.0 identifiers,
1224which is the default. If you want, you can set this property to 1
1225to make this behavior explicit.
1226
1227=item $csr->B<message>($key)
1228
1229Obtain a value from the message contained in the request arguments
1230with the given key. This can only be used to obtain core arguments,
1231not extension arguments.
1232
1233Call this method without a C<$key> argument to get a L<Net::OpenID::IndirectMessage>
1234object representing the message.
1235
1236=item $csr->B<args>($ref)
1237
1238=item $csr->B<args>($param)
1239
1240=item $csr->B<args>
1241
1242Can be used in 1 of 3 ways:
1243
12441. Setting the way which the Consumer instances obtains GET parameters:
1245
1246$csr->args( $reference )
1247
1248Where $reference is either a HASH ref, CODE ref, Apache $r,
1249Apache::Request $apreq, or CGI.pm $cgi.  If a CODE ref, the subref
1250must return the value given one argument (the parameter to retrieve)
1251
1252If you pass in an Apache $r object, you must not have already called
1253$r->content as the consumer module will want to get the request
1254arguments out of here in the case of a POST request.
1255
12562. Get a paramater:
1257
1258my $foo = $csr->args("foo");
1259
1260When given an unblessed scalar, it retrieves the value.  It croaks if
1261you haven't defined a way to get at the parameters.
1262
1263Most callers should instead use the C<message> method above, which
1264abstracts away the need to understand OpenID's message serialization.
1265
12663. Get the getter:
1267
1268my $code = $csr->args;
1269
1270Without arguments, returns a subref that returns the value given a
1271parameter name.
1272
1273Most callers should instead use the C<message> method above with no
1274arguments, which returns an object from which extension attributes
1275can be obtained by their documented namespace URI.
1276
1277=item $nos->B<required_root>($url_prefix)
1278
1279=item $url_prefix = $nos->B<required_root>
1280
1281If provided, this is the required string that all return_to URLs must
1282start with.  If it doesn't match, it'll be considered invalid (spoofed
1283from another site)
1284
1285=item $csr->B<claimed_identity>($url)
1286
1287Given a user-entered $url (which could be missing http://, or have
1288extra whitespace, etc), returns either a Net::OpenID::ClaimedIdentity
1289object, or undef on failure.
1290
1291Note that this identity is NOT verified yet.  It's only who the user
1292claims they are, but they could be lying.
1293
1294If this method returns undef, you can rely on the following errors
1295codes (from $csr->B<errcode>) to decide what to present to the user:
1296
1297=over 8
1298
1299=item no_identity_server
1300
1301=item empty_url
1302
1303=item bogus_url
1304
1305=item no_head_tag
1306
1307=item url_fetch_err
1308
1309=back
1310
1311=item $csr->B<handle_server_response>( %callbacks );
1312
1313When a request comes in that contains a response from an OpenID provider,
1314figure out what it means and dispatch to an appropriate callback to handle
1315the request. This is the callback-based alternative to explicitly calling
1316the methods below in the correct sequence, and is recommended unless you
1317need to do something strange.
1318
1319Anything you return from the selected callback function will be returned
1320by this method verbatim. This is useful if the caller needs to return
1321something different in each case.
1322
1323The available callbacks are:
1324
1325=over 8
1326
1327=item B<not_openid> - the request isn't an OpenID response after all.
1328
1329=item B<setup_required>($setup_url) - the provider needs to present some UI to the user before it can respond. Send the user to the given URL by some means.
1330
1331=item B<cancelled> - the user cancelled the authentication request from the provider's UI
1332
1333=item B<verified>($verified_identity) - the user's identity has been successfully verified. A L<Net::OpenID::VerifiedIdentity> object is passed in.
1334
1335=item B<error>($errcode, $errmsg) - an error has occured. An error code and message are provided.
1336
1337=back
1338
1339=item $csr->B<user_setup_url>( [ %opts ] )
1340
1341Returns the URL the user must return to in order to login, setup trust,
1342or do whatever the identity server needs them to do in order to make
1343the identity assertion which they previously initiated by entering
1344their claimed identity URL.  Returns undef if this setup URL isn't
1345required, in which case you should ask for the verified_identity.
1346
1347The base URL this this function returns can be modified by using the
1348following options in %opts:
1349
1350=over
1351
1352=item C<post_grant>
1353
1354What you're asking the identity server to do with the user after they
1355setup trust.  Can be either C<return> or C<close> to return the user
1356back to the return_to URL, or close the browser window with
1357JavaScript.  If you don't specify, the behavior is undefined (probably
1358the user gets a dead-end page with a link back to the return_to URL).
1359In any case, the identity server can do whatever it wants, so don't
1360depend on this.
1361
1362=back
1363
1364=item $csr->B<user_cancel>
1365
1366Returns true if the user declined to share their identity, false
1367otherwise.  (This function is literally one line: returns true if
1368"openid.mode" eq "cancel")
1369
1370It's then your job to restore your app to where it was prior to
1371redirecting them off to the user_setup_url, using the other query
1372parameters that you'd sent along in your return_to URL.
1373
1374=item $csr->B<verified_identity>( [ %opts ] )
1375
1376Returns a Net::OpenID::VerifiedIdentity object, or undef.
1377Verification includes double-checking the reported identity URL
1378declares the identity server, verifying the signature, etc.
1379
1380The options in %opts may contain:
1381
1382=over
1383
1384=item C<required_root>
1385
1386Sets the required_root just for this request.  Values returns to its
1387previous value afterwards.
1388
1389=back
1390
1391=item $csr->B<err>
1392
1393Returns the last error, in form "errcode: errtext"
1394
1395=item $csr->B<errcode>
1396
1397Returns the last error code.
1398
1399=item $csr->B<errtext>
1400
1401Returns the last error text.
1402
1403=item $csr->B<json_err>
1404
1405Returns the last error code/text in JSON format.
1406
1407=back
1408
1409=head1 COPYRIGHT
1410
1411This module is Copyright (c) 2005 Brad Fitzpatrick.
1412All rights reserved.
1413
1414You may distribute under the terms of either the GNU General Public
1415License or the Artistic License, as specified in the Perl README file.
1416If you need more liberal licensing terms, please contact the
1417maintainer.
1418
1419=head1 WARRANTY
1420
1421This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND.
1422
1423=head1 MAILING LIST
1424
1425The Net::OpenID family of modules has a mailing list powered
1426by Google Groups. For more information, see
1427http://groups.google.com/group/openid-perl .
1428
1429=head1 SEE ALSO
1430
1431OpenID website: http://openid.net/
1432
1433L<Net::OpenID::ClaimedIdentity> -- part of this module
1434
1435L<Net::OpenID::VerifiedIdentity> -- part of this module
1436
1437L<Net::OpenID::Server> -- another module, for acting like an OpenID server
1438
1439=head1 AUTHORS
1440
1441Brad Fitzpatrick <brad@danga.com>
1442
1443Tatsuhiko Miyagawa <miyagawa@sixapart.com>
1444
1445Martin Atkins <mart@degeneration.co.uk>
1446
Note: See TracBrowser for help on using the browser.