File Coverage

blib/lib/Net/OpenID/Consumer.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             # LICENSE: You're free to distribute this under the same terms as Perl itself.
2              
3 6     6   114655 use strict;
  6         13  
  6         147  
4 6     6   31 use Carp ();
  6         9  
  6         500  
5              
6             ############################################################################
7             package Net::OpenID::Consumer;
8             {
9             $Net::OpenID::Consumer::VERSION = '1.16';
10             }
11              
12              
13             use fields (
14 6         35 'cache', # Cache object to store HTTP responses,
15             # associations, and nonces
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             'assoc_options', # options for establishing ID provider associations
26             'nonce_options', # options for dealing with nonces
27 6     6   4605 );
  6         9333  
28              
29 6     6   3993 use Net::OpenID::ClaimedIdentity;
  6         16  
  6         244  
30 6     6   3506 use Net::OpenID::VerifiedIdentity;
  6         19  
  6         187  
31 6     6   3644 use Net::OpenID::Association;
  0            
  0            
32             use Net::OpenID::Yadis;
33             use Net::OpenID::IndirectMessage;
34             use Net::OpenID::URIFetch;
35             use Net::OpenID::Common; # To get the OpenID::util package
36              
37             use MIME::Base64 ();
38             use Digest::SHA qw(hmac_sha1_hex);
39             use Time::Local;
40             use HTTP::Request;
41             use LWP::UserAgent;
42             use Storable;
43             use JSON qw(encode_json);
44             use URI::Escape qw(uri_escape_utf8);
45             use HTML::Parser;
46              
47             sub new {
48             my Net::OpenID::Consumer $self = shift;
49             $self = fields::new( $self ) unless ref $self;
50             my %opts = @_;
51              
52             $self->{ua} = delete $opts{ua};
53             $self->args ( delete $opts{args} );
54             $self->cache ( delete $opts{cache} );
55             $self->consumer_secret ( delete $opts{consumer_secret} );
56             $self->required_root ( delete $opts{required_root} );
57             $self->minimum_version ( delete $opts{minimum_version} );
58             $self->assoc_options ( delete $opts{assoc_options} );
59             $self->nonce_options ( delete $opts{nonce_options} );
60              
61             $self->{debug} = delete $opts{debug};
62              
63             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
64             return $self;
65             }
66              
67             # NOTE: This method is here only to support the openid-test library.
68             # Don't call it from anywhere else, or you'll break when it gets
69             # removed. Instead, call minimum_version(2).
70             # FIXME: Can we just make openid-test do that and get rid of this?
71             sub disable_version_1 {
72             $_[0]->minimum_version(2);
73             }
74              
75             sub cache { &_getset; }
76             sub consumer_secret { &_getset; }
77             sub required_root { &_getset; }
78             sub assoc_options { &_hashgetset }
79             sub nonce_options { &_hashgetset }
80              
81             sub _getset {
82             my Net::OpenID::Consumer $self = shift;
83             my $param = (caller(1))[3];
84             $param =~ s/.+:://;
85              
86             if (@_) {
87             my $val = shift;
88             Carp::croak("Too many parameters") if @_;
89             $self->{$param} = $val;
90             }
91             return $self->{$param};
92             }
93              
94             sub _hashgetset {
95             my Net::OpenID::Consumer $self = shift;
96             my $param = (caller(1))[3];
97             $param =~ s/.+:://;
98             my $check_param = "_canonicalize_$param";
99              
100             my $v;
101             if (scalar(@_) == 1) {
102             $v = shift;
103             unless ($v) {
104             $v = {};
105             }
106             elsif (ref $v eq 'ARRAY') {
107             $v = {@$v};
108             }
109             elsif (ref $v) {
110             # assume it's a hash and hope for the best
111             $v = {%$v};
112             }
113             else {
114             Carp::croak("single argument must be HASH or ARRAY reference");
115             }
116             $self->{$param} = $self->$check_param($v);
117             }
118             elsif (@_) {
119             Carp::croak("odd number of parameters?")
120             if scalar(@_)%2;
121             $self->{$param} = $self->$check_param({@_});
122             }
123             return $self->{$param};
124             }
125              
126             sub minimum_version {
127             my Net::OpenID::Consumer $self = shift;
128              
129             if (@_) {
130             my $minv = shift;
131             Carp::croak("Too many parameters") if @_;
132             $minv = 1 unless $minv && $minv > 1;
133             $self->{minimum_version} = $minv;
134             }
135             return $self->{minimum_version};
136             }
137              
138             sub _canonicalize_assoc_options { return $_[1]; }
139              
140             sub _debug {
141             my Net::OpenID::Consumer $self = shift;
142             return unless $self->{debug};
143              
144             if (ref $self->{debug} eq "CODE") {
145             $self->{debug}->($_[0]);
146             } else {
147             print STDERR "[DEBUG Net::OpenID::Consumer] $_[0]\n";
148             }
149             }
150              
151             # given something that can have GET arguments, returns a subref to get them:
152             # Apache
153             # Apache::Request
154             # CGI
155             # HASH of get args
156             # CODE returning get arg, given key
157              
158             # ...
159              
160             sub args {
161             my Net::OpenID::Consumer $self = shift;
162              
163             if (my $what = shift) {
164             unless (ref $what) {
165             return $self->{args} ? $self->{args}->($what) : Carp::croak("No args defined");
166             }
167             Carp::croak("Too many parameters") if @_;
168              
169             # since we do not require field setters to be called in any particular order,
170             # we cannot pass minimum_version here as it might change later.
171             my $message = Net::OpenID::IndirectMessage->new($what);
172             $self->{message} = $message;
173             if ($message) {
174             $self->{args} = $message->getter;
175              
176             # handle OpenID 2.0 'error' mode
177             # (may as well do this here; we may not get another chance
178             # since handle_server_response is not a required part of the API)
179             if ($message->protocol_version >= 2 && $message->mode eq 'error') {
180             $self->_fail('provider_error',$message->get('error'));
181             }
182             }
183             else {
184             $self->{args} = sub { undef };
185             }
186             }
187             $self->{args};
188             }
189              
190             sub message {
191             my Net::OpenID::Consumer $self = shift;
192             my $message = $self->{message};
193             return undef
194             unless $message &&
195             ($self->{minimum_version} <= $message->protocol_version);
196              
197             if (@_) {
198             return $message->get($_[0]);
199             }
200             else {
201             return $message;
202             }
203             }
204              
205             sub _message_mode_is {
206             return (($_[0]->message('mode')||' ') eq $_[1]);
207             }
208              
209             sub _message_version {
210             my $message = $_[0]->message;
211             return $message ? $message->protocol_version : 0;
212             }
213              
214             sub ua {
215             my Net::OpenID::Consumer $self = shift;
216             $self->{ua} = shift if @_;
217             Carp::croak("Too many parameters") if @_;
218              
219             # make default one on first access
220             unless ($self->{ua}) {
221             my $ua = $self->{ua} = LWP::UserAgent->new;
222             $ua->timeout(10);
223             }
224              
225             $self->{ua};
226             }
227              
228             our %Error_text =
229             (
230             'bad_mode' => "The openid.mode argument is not correct",
231             'bogus_delegation' => "Asserted identity does not match claimed_id or local_id.",
232             'bogus_return_to' => "Return URL does not match required_root.",
233             'bogus_url' => "URL scheme must be http: or https:",
234             'empty_url' => "No URL entered.",
235             'expired_association' => "Association between ID provider and relying party has expired.",
236             'naive_verify_failed_network' => "Could not contact ID provider to verify response.",
237             'naive_verify_failed_return' => "Direct contact invalidated ID provider response.",
238             'no_identity' => "Identity is missing from ID provider response.",
239             'no_identity_server' => "Could not determine ID provider from URL.",
240             'no_return_to' => "Return URL is missing from ID provider response.",
241             'no_sig' => "Signature is missing from ID provider response.",
242             'protocol_version_incorrect' => "ID provider does not support minimum protocol version",
243             'provider_error' => "ID provider-specific error",
244             'server_not_allowed' => "None of the discovered endpoints matches op_endpoint.",
245             'signature_mismatch' => "Prior association invalidated ID provider response.",
246             'time_bad_sig' => "Return_to signature is not valid.",
247             'time_expired' => "Return_to signature is stale.",
248             'time_in_future' => "Return_to signature is from the future.",
249             'unexpected_url_redirect' => "Discovery for the given ID ended up at the wrong place",
250             'unsigned_field' => sub { "Field(s) must be signed: " . join(", ", @_) },
251             'nonce_missing' => "Response_nonce is missing from ID provider response.",
252             'nonce_reused' => 'Re-used response_nonce; possible replay attempt.',
253             'nonce_stale' => 'Stale response_nonce; could have been used before.',
254             'nonce_format' => 'Bad timestamp format in response_nonce.',
255             'nonce_future' => 'Provider clock is too far forward.',
256              
257             # no longer used as of 1.11
258             # 'no_head_tag' => "Could not determine ID provider; URL document has no .",
259             # 'url_fetch_err' => "Error fetching the provided URL.",
260              
261             );
262              
263             sub _fail {
264             my Net::OpenID::Consumer $self = shift;
265             my ($code, $text, @params) = @_;
266              
267             # 'bad_mode' is only an error if we survive to the end of
268             # .mode dispatch without having figured out what to do;
269             # it should not overwrite other errors.
270             unless ($self->{last_errcode} && $code eq 'bad_mode') {
271             $text ||= $Error_text{$code};
272             $text = $text->(@params) if ref($text) && ref($text) eq 'CODE';
273             $self->{last_errcode} = $code;
274             $self->{last_errtext} = $text;
275             $self->_debug("fail($code) $text");
276             }
277             wantarray ? () : undef;
278             }
279              
280             sub json_err {
281             my Net::OpenID::Consumer $self = shift;
282             return encode_json({
283             err_code => $self->{last_errcode},
284             err_text => $self->{last_errtext},
285             });
286             }
287              
288             sub err {
289             my Net::OpenID::Consumer $self = shift;
290             $self->{last_errcode} . ": " . $self->{last_errtext};
291             }
292              
293             sub errcode {
294             my Net::OpenID::Consumer $self = shift;
295             $self->{last_errcode};
296             }
297              
298             sub errtext {
299             my Net::OpenID::Consumer $self = shift;
300             $self->{last_errtext};
301             }
302              
303             # make sure you change the $prefix every time you change the $hook format
304             # so that when user installs a new version and the old cache server is
305             # still running the old cache entries won't confuse things.
306             sub _get_url_contents {
307             my Net::OpenID::Consumer $self = shift;
308             my ($url, $final_url_ref, $hook, $prefix) = @_;
309             $final_url_ref ||= do { my $dummy; \$dummy; };
310              
311             my $res = Net::OpenID::URIFetch->fetch($url, $self, $hook, $prefix);
312              
313             $$final_url_ref = $res->final_uri;
314              
315             return $res ? $res->content : undef;
316             }
317              
318              
319             # List of head elements that matter for HTTP discovery.
320             # Each entry defines a key+value that will appear in the
321             # _find_semantic_info hash if the specified element exists
322             # [
323             # FSI_KEY -- key name
324             # TAG_NAME -- must be 'link' or 'meta'
325             #
326             # ELT_VALUES -- string (default = FSI_KEY)
327             # what join(';',values of ELT_KEYS) has to match
328             # in order for a given html element to provide
329             # the value for FSI_KEY
330             #
331             # ELT_KEYS -- list-ref of html attribute names
332             # default = ['rel'] for
333             # default = ['name'] for
334             #
335             # FSI_VALUE -- name of html attribute where value lives
336             # default = 'href' for
337             # default = 'content' for
338             # ]
339             #
340             our @HTTP_discovery_link_meta_tags =
341             map {
342             my ($fsi_key, $tag, $elt_value, $elt_keys, $fsi_value) = @{$_};
343             [$fsi_key, $tag,
344             $elt_value || $fsi_key,
345             $elt_keys || [$tag eq 'link' ? 'rel' : 'name'],
346             $fsi_value || ($tag eq 'link' ? 'href' : 'content'),
347             ]
348             }
349             # OpenID providers / delegated identities
350             #
351             # href="http://www.livejournal.com/misc/openid.bml" />
352             #
353             # href="whatever" />
354             #
355             [qw(openid.server link)], # 'openid.server' => ['rel'], 'href'
356             [qw(openid.delegate link)],
357              
358             # OpenID2 providers / local identifiers
359             #
360             # href="http://www.livejournal.com/misc/openid.bml" />
361             #
362             #
363             [qw(openid2.provider link)],
364             [qw(openid2.local_id link)],
365              
366             # FOAF maker info
367             #
368             # content="foaf:mbox_sha1sum '4caa1d6f6203d21705a00a7aca86203e82a9cf7a'"/>
369             #
370             [qw(foaf.maker meta foaf:maker)], # == .name
371              
372             # FOAF documents
373             #
374             # href="http://brad.livejournal.com/data/foaf" />
375             #
376             [qw(foaf link), 'meta;foaf;application/rdf+xml' => [qw(rel title type)]],
377              
378             # RSS
379             #
380             # href="http://www.livejournal.com/~brad/data/rss" />
381             #
382             [qw(rss link), 'alternate;application/rss+xml' => [qw(rel type)]],
383              
384             # Atom
385             #
386             # href="http://www.livejournal.com/~brad/data/rss" />
387             #
388             [qw(atom link), 'alternate;application/atom+xml' => [qw(rel type)]],
389             ;
390              
391             sub _document_to_semantic_info {
392             my $doc = shift;
393             my $info = {};
394              
395             my $elts = OpenID::util::html_extract_linkmetas($doc);
396             for (@HTTP_discovery_link_meta_tags) {
397             my ($key, $tag, $elt_value, $elt_keys, $vattrib) = @$_;
398             for my $lm (@{$elts->{$tag}}) {
399             $info->{$key} = $lm->{$vattrib}
400             if $elt_value eq join ';', map {lc($lm->{$_}||'')} @$elt_keys;
401             }
402             }
403             return $info;
404             }
405              
406             sub _find_semantic_info {
407             my Net::OpenID::Consumer $self = shift;
408             my $url = shift;
409             my $final_url_ref = shift;
410              
411             my $doc = $self->_get_url_contents($url, $final_url_ref);
412             my $info = _document_to_semantic_info($doc);
413             $self->_debug("semantic info ($url) = " . join(", ", map { $_.' => '.$info->{$_} } keys %$info)) if $self->{debug};
414              
415             return $info;
416             }
417              
418             sub _find_openid_server {
419             my Net::OpenID::Consumer $self = shift;
420             my $url = shift;
421             my $final_url_ref = shift;
422              
423             my $sem_info = $self->_find_semantic_info($url, $final_url_ref) or
424             return;
425              
426             return $self->_fail("no_identity_server") unless $sem_info->{"openid.server"};
427             $sem_info->{"openid.server"};
428             }
429              
430             sub is_server_response {
431             my Net::OpenID::Consumer $self = shift;
432             return $self->message ? 1 : 0;
433             }
434              
435             my $_warned_about_setup_required = 0;
436             sub handle_server_response {
437             my Net::OpenID::Consumer $self = shift;
438             my %callbacks_in = @_;
439             my %callbacks = ();
440              
441             foreach my $cb (qw(not_openid cancelled verified error)) {
442             $callbacks{$cb} = delete($callbacks_in{$cb}) || sub { Carp::croak("No ".$cb." callback") };
443             }
444              
445             # backwards compatibility:
446             # 'setup_needed' is expected as of 1.04
447             # 'setup_required' is deprecated but allowed in its place,
448             my $found_setup_callback = 0;
449             foreach my $cb (qw(setup_needed setup_required)) {
450             $callbacks{$cb} = delete($callbacks_in{$cb}) and $found_setup_callback++;
451             }
452             Carp::croak($found_setup_callback > 1
453             ? "Cannot have both setup_needed and setup_required"
454             : "No setup_needed callback")
455             unless $found_setup_callback == 1;
456              
457             if (warnings::enabled('deprecated') &&
458             $callbacks{setup_required} &&
459             !$_warned_about_setup_required++
460             ) {
461             warnings::warn
462             ("deprecated",
463             "'setup_required' callback is deprecated, use 'setup_needed'");
464             }
465              
466             Carp::croak("Unknown callbacks: ".join(',', keys %callbacks_in))
467             if %callbacks_in;
468              
469             unless ($self->is_server_response) {
470             return $callbacks{not_openid}->();
471             }
472              
473             if ($self->setup_needed) {
474             return $callbacks{setup_needed}->()
475             unless ($callbacks{setup_required});
476              
477             my $setup_url = $self->user_setup_url;
478             return $callbacks{setup_required}->($setup_url)
479             if $setup_url;
480             # otherwise FALL THROUGH to preserve prior behavior,
481             # Even though this is broken, old clients could have
482             # put a workaround into the 'error' callback to handle
483             # the setup_needed+(setup_url=undef) case
484             }
485              
486             if ($self->user_cancel) {
487             return $callbacks{cancelled}->();
488             }
489             elsif (my $vident = $self->verified_identity) {
490             return $callbacks{verified}->($vident);
491             }
492             else {
493             return $callbacks{error}->($self->errcode, $self->errtext);
494             }
495              
496             }
497              
498             sub _canonicalize_id_url {
499             my Net::OpenID::Consumer $self = shift;
500             my $url = shift;
501              
502             # trim whitespace
503             $url =~ s/^\s+//;
504             $url =~ s/\s+$//;
505             return $self->_fail("empty_url") unless $url;
506              
507             # add scheme
508             $url = "http://$url" if $url && $url !~ m!^\w+://!;
509             return $self->_fail("bogus_url") unless $url =~ m!^https?://!i;
510              
511             # make sure there is a slash after the hostname
512             $url .= "/" unless $url =~ m!^https?://.+/!i;
513             return $url;
514             }
515              
516             # always returns a listref; might be empty, though
517             sub _discover_acceptable_endpoints {
518             my Net::OpenID::Consumer $self = shift;
519             my $url = shift; #already canonicalized ID url
520             my %opts = @_;
521              
522             # if return_early is set, we'll return as soon as we have enough
523             # information to determine the "primary" endpoint, and return
524             # that as the first (and possibly only) item in our response.
525             my $primary_only = delete $opts{primary_only} ? 1 : 0;
526              
527             # if force_version is set, we only return endpoints that have
528             # that have {version} == $force_version
529             my $force_version = delete $opts{force_version};
530              
531             Carp::croak("Unknown option(s) ".join(', ', keys(%opts))) if %opts;
532              
533             my @discovered_endpoints = ();
534             my $result = sub {
535             # We always prefer 2.0 endpoints to 1.1 ones, regardless of
536             # the priority chosen by the identifier.
537             return [
538             (grep { $_->{version} == 2 } @discovered_endpoints),
539             (grep { $_->{version} == 1 } @discovered_endpoints),
540             ];
541             };
542              
543             # TODO: Support XRI too?
544              
545             # First we Yadis service discovery
546             my $yadis = Net::OpenID::Yadis->new(consumer => $self);
547             if ($yadis->discover($url)) {
548             # FIXME: Currently we don't ever do _find_semantic_info in the Yadis
549             # code path, so an extra redundant HTTP request is done later
550             # when the semantic info is accessed.
551              
552             my $final_url = $yadis->identity_url;
553             my @services = $yadis->services(
554             OpenID::util::version_2_xrds_service_url(),
555             OpenID::util::version_2_xrds_directed_service_url(),
556             OpenID::util::version_1_xrds_service_url(),
557             );
558             my $version2 = OpenID::util::version_2_xrds_service_url();
559             my $version1 = OpenID::util::version_1_xrds_service_url();
560             my $version2_directed = OpenID::util::version_2_xrds_directed_service_url();
561              
562             foreach my $service (@services) {
563             my $service_uris = $service->URI;
564              
565             # Service->URI seems to return all sorts of bizarre things, so let's
566             # normalize it to always be an arrayref.
567             if (ref($service_uris) eq 'ARRAY') {
568             my @sorted_id_servers = sort {
569             my $pa = $a->{priority};
570             my $pb = $b->{priority};
571             defined($pb) <=> defined($pa)
572             || (defined($pa) ? ($pa <=> $pb) : 0)
573             } @$service_uris;
574             $service_uris = \@sorted_id_servers;
575             }
576             if (ref($service_uris) eq 'HASH') {
577             $service_uris = [ $service_uris->{content} ];
578             }
579             unless (ref($service_uris)) {
580             $service_uris = [ $service_uris ];
581             }
582              
583             my $delegate = undef;
584             my @versions = ();
585              
586             if (grep(/^${version2}$/, $service->Type)) {
587             # We have an OpenID 2.0 end-user identifier
588             $delegate = $service->extra_field("LocalID");
589             push @versions, 2;
590             }
591             if (grep(/^${version1}$/, $service->Type)) {
592             # We have an OpenID 1.1 end-user identifier
593             $delegate = $service->extra_field("Delegate", "http://openid.net/xmlns/1.0");
594             push @versions, 1;
595             }
596              
597             if (@versions) {
598             foreach my $version (@versions) {
599             next if defined($force_version) && $force_version != $version;
600             foreach my $uri (@$service_uris) {
601             push @discovered_endpoints, {
602             uri => $uri,
603             version => $version,
604             final_url => $final_url,
605             delegate => $delegate,
606             sem_info => undef,
607             mechanism => "Yadis",
608             };
609             }
610             }
611             }
612              
613             if (((!defined($force_version)) || $force_version == 2)
614             && grep(/^${version2_directed}$/, $service->Type)) {
615              
616             # We have an OpenID 2.0 OP identifier (i.e. we're doing directed identity)
617             my $version = 2;
618             # In this case, the user's claimed identifier is a magic value
619             # and the actual identifier will be determined by the provider.
620             my $final_url = OpenID::util::version_2_identifier_select_url();
621             my $delegate = OpenID::util::version_2_identifier_select_url();
622              
623             foreach my $uri (@$service_uris) {
624             push @discovered_endpoints, {
625             uri => $uri,
626             version => $version,
627             final_url => $final_url,
628             delegate => $delegate,
629             sem_info => undef,
630             mechanism => "Yadis",
631             };
632             }
633             }
634              
635             if ($primary_only && scalar(@discovered_endpoints)) {
636             # We've got at least one endpoint now, so return early
637             return $result->();
638             }
639             }
640             }
641              
642             # Now HTML-based discovery, both 2.0- and 1.1-style.
643             {
644             my $final_url = undef;
645             my $sem_info = $self->_find_semantic_info($url, \$final_url);
646              
647             if ($sem_info) {
648             if ($sem_info->{"openid2.provider"}) {
649             unless (defined($force_version) && $force_version != 2) {
650             push @discovered_endpoints, {
651             uri => $sem_info->{"openid2.provider"},
652             version => 2,
653             final_url => $final_url,
654             delegate => $sem_info->{"openid2.local_id"},
655             sem_info => $sem_info,
656             mechanism => "HTML",
657             };
658             }
659             }
660             if ($sem_info->{"openid.server"}) {
661             unless (defined($force_version) && $force_version != 1) {
662             push @discovered_endpoints, {
663             uri => $sem_info->{"openid.server"},
664             version => 1,
665             final_url => $final_url,
666             delegate => $sem_info->{"openid.delegate"},
667             sem_info => $sem_info,
668             mechanism => "HTML",
669             };
670             }
671             }
672             }
673             }
674              
675             return $result->();
676              
677             }
678              
679             # returns Net::OpenID::ClaimedIdentity
680             sub claimed_identity {
681             my Net::OpenID::Consumer $self = shift;
682             my $url = shift;
683             Carp::croak("Too many parameters") if @_;
684              
685             return unless $url = $self->_canonicalize_id_url($url);
686              
687             my $endpoints = $self->_discover_acceptable_endpoints($url, primary_only => 1);
688              
689             if (@$endpoints) {
690             foreach my $endpoint (@$endpoints) {
691              
692             next unless $endpoint->{version} >= $self->minimum_version;
693              
694             $self->_debug("Discovered version $endpoint->{version} endpoint at $endpoint->{uri} via $endpoint->{mechanism}");
695             $self->_debug("Delegate is $endpoint->{delegate}") if $endpoint->{delegate};
696              
697             return Net::OpenID::ClaimedIdentity->new(
698             identity => $endpoint->{final_url},
699             server => $endpoint->{uri},
700             consumer => $self,
701             delegate => $endpoint->{delegate},
702             protocol_version => $endpoint->{version},
703             semantic_info => $endpoint->{sem_info},
704             );
705              
706             }
707              
708             # If we've fallen out here, then none of the available services are of the required version.
709             return $self->_fail("protocol_version_incorrect");
710              
711             }
712             else {
713             return $self->_fail("no_identity_server");
714             }
715              
716             }
717              
718             sub user_cancel {
719             my Net::OpenID::Consumer $self = shift;
720             return $self->_message_mode_is("cancel");
721             }
722              
723             sub setup_needed {
724             my Net::OpenID::Consumer $self = shift;
725             if ($self->_message_version == 1) {
726             return $self->_message_mode_is("id_res") && $self->message("user_setup_url");
727             }
728             else {
729             return $self->_message_mode_is('setup_needed');
730             }
731             }
732              
733             sub user_setup_url {
734             my Net::OpenID::Consumer $self = shift;
735             my %opts = @_;
736             my $post_grant = delete $opts{'post_grant'};
737             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
738              
739             if ($self->_message_version == 1) {
740             return $self->_fail("bad_mode") unless $self->_message_mode_is("id_res");
741             }
742             else {
743             return undef unless $self->_message_mode_is('setup_needed');
744             }
745             my $setup_url = $self->message("user_setup_url");
746              
747             OpenID::util::push_url_arg(\$setup_url, "openid.post_grant", $post_grant)
748             if $setup_url && $post_grant;
749              
750             return $setup_url;
751             }
752              
753             sub verified_identity {
754             my Net::OpenID::Consumer $self = shift;
755             my %opts = @_;
756              
757             my $rr = delete $opts{'required_root'} || $self->{required_root};
758             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
759              
760             return $self->_fail("bad_mode") unless $self->_message_mode_is("id_res");
761              
762             # the asserted identity (the delegated one, if there is one, since the protocol
763             # knows nothing of the original URL)
764             my $a_ident = $self->message("identity") or return $self->_fail("no_identity");
765              
766             my $sig64 = $self->message("sig") or return $self->_fail("no_sig");
767              
768             # fix sig if the OpenID provider failed to properly escape pluses (+) in the sig
769             $sig64 =~ s/ /+/g;
770              
771             my $returnto = $self->message("return_to") or return $self->_fail("no_return_to");
772             my $signed = $self->message("signed");
773              
774             my $possible_endpoints;
775             my $server;
776             my $claimed_identity;
777              
778             my $real_ident =
779             ($self->_message_version == 1
780             ? $self->args("oic.identity")
781             : $self->message("claimed_id")
782             ) || $a_ident;
783             my $real_canon = $self->_canonicalize_id_url($real_ident);
784              
785             return $self->_fail("no_identity_server")
786             unless ($real_canon
787             && @{
788             $possible_endpoints =
789             $self->_discover_acceptable_endpoints
790             ($real_canon, force_version => $self->_message_version)
791             });
792             # FIXME: It kinda sucks that the above will always do both Yadis and HTML discovery, even though
793             # in most cases only one will be in use.
794              
795             if ($self->_message_version == 1) {
796             # In version 1, we have to assume that the primary server
797             # found during discovery is the one sending us this message.
798             splice(@$possible_endpoints,1);
799             $server = $possible_endpoints->[0]->{uri};
800             $self->_debug("Server is $server");
801             }
802             else {
803             # In version 2, the OpenID provider tells us its URL.
804             $server = $self->message("op_endpoint");
805             $self->_debug("Server is $server");
806             # but make sure that URL matches one of the discovered ones.
807             @$possible_endpoints =
808             grep {$_->{uri} eq $server} @$possible_endpoints
809             or return $self->_fail("server_not_allowed");
810             }
811              
812             # check that returnto is for the right host
813             return $self->_fail("bogus_return_to") if $rr && $returnto !~ /^\Q$rr\E/;
814              
815             my $now = time();
816              
817             # check that we have not seen response_nonce before
818             my $response_nonce = $self->message("response_nonce");
819             unless ($response_nonce) {
820             # 1.0/1.1 does not require nonces
821             return $self->_fail("nonce_missing")
822             if $self->_message_version >= 2;
823             }
824             else {
825             return unless $self->_nonce_check_succeeds($now, $server, $response_nonce);
826             }
827              
828             # check age/signature of return_to
829             {
830             my ($sig_time, $sig) = split(/\-/, $self->args("oic.time") || "");
831             # complain if more than an hour since we sent them off
832             return $self->_fail("time_expired") if $sig_time < $now - 3600;
833             # also complain if the signature is from the future by more than 30 seconds,
834             # which compensates for potential clock drift between nodes in a web farm.
835             return $self->_fail("time_in_future") if $sig_time - 30 > $now;
836             # and check that the time isn't faked
837             my $c_secret = $self->_get_consumer_secret($sig_time);
838             my $good_sig = substr(hmac_sha1_hex($sig_time, $c_secret), 0, 20);
839             return $self->_fail("time_bad_sig") unless OpenID::util::timing_indep_eq($sig, $good_sig);
840             }
841              
842             my $last_error = undef;
843             my $error = sub {
844             $self->_debug("$server not acceptable: ".$_[0]);
845             $last_error = $_[0];
846             };
847              
848             foreach my $endpoint (@$possible_endpoints) {
849             # Known:
850             # $endpoint->{version} == $self->_message_version
851             # $endpoint->{uri} == $server
852              
853             my $final_url = $endpoint->{final_url};
854             my $delegate = $endpoint->{delegate};
855              
856             # OpenID 2.0 wants us to exclude the fragment part of the URL when doing equality checks
857             my $a_ident_nofragment = $a_ident;
858             my $real_ident_nofragment = $real_ident;
859             my $final_url_nofragment = $final_url;
860             if ($self->_message_version >= 2) {
861             $a_ident_nofragment =~ s/\#.*$//x;
862             $real_ident_nofragment =~ s/\#.*$//x;
863             $final_url_nofragment =~ s/\#.*$//x;
864             }
865             unless ($final_url_nofragment eq $real_ident_nofragment) {
866             $error->("unexpected_url_redirect");
867             next;
868             }
869              
870             # if openid.delegate was used, check that it was done correctly
871             if ($a_ident_nofragment ne $real_ident_nofragment) {
872             unless ($delegate eq $a_ident_nofragment) {
873             $error->("bogus_delegation");
874             next;
875             }
876             }
877              
878             # If we've got this far then we've found the right endpoint.
879              
880             $claimed_identity = Net::OpenID::ClaimedIdentity->new(
881             identity => $endpoint->{final_url},
882             server => $endpoint->{uri},
883             consumer => $self,
884             delegate => $endpoint->{delegate},
885             protocol_version => $endpoint->{version},
886             semantic_info => $endpoint->{sem_info},
887             );
888             last;
889              
890             }
891              
892             unless ($claimed_identity) {
893             # We failed to find a good endpoint in the above loop, so
894             # lets bail out.
895             return $self->_fail($last_error);
896             }
897              
898             my $assoc_handle = $self->message("assoc_handle");
899              
900             $self->_debug("verified_identity: assoc_handle" .
901             ($assoc_handle ? ": $assoc_handle" : " missing"));
902             my $assoc = Net::OpenID::Association::handle_assoc($self, $server, $assoc_handle);
903              
904             my @signed_fields = grep {m/^[\w\.]+$/} split(/,/, $signed);
905             my %signed_value = map {$_,$self->args("openid.$_")} @signed_fields;
906              
907             # Auth 2.0 requires certain keys to be signed.
908             if ($self->_message_version >= 2) {
909             my %unsigned;
910             # these fields must be signed unconditionally
911             foreach my $f (qw/op_endpoint return_to response_nonce assoc_handle/) {
912             $unsigned{$f}++ unless exists $signed_value{$f};
913             }
914             # these fields must be signed if present
915             foreach my $f (qw/claimed_id identity/) {
916             $unsigned{$f}++
917             if $self->args("openid.$f") && !exists $signed_value{$f};
918             }
919             if (%unsigned) {
920             return $self->_fail("unsigned_field", undef, keys %unsigned);
921             }
922             }
923              
924             if ($assoc) {
925             $self->_debug("verified_identity: verifying with found association");
926              
927             return $self->_fail("expired_association")
928             if $assoc->expired;
929              
930             # verify the token
931             my $token = join '',map {"$_:$signed_value{$_}\n"} @signed_fields;
932              
933             utf8::encode($token);
934             my $good_sig = $assoc->generate_signature($token);
935             return $self->_fail("signature_mismatch") unless OpenID::util::timing_indep_eq($sig64, $good_sig);
936              
937             } else {
938             $self->_debug("verified_identity: verifying using HTTP (dumb mode)");
939             # didn't find an association. have to do dumb consumer mode
940             # and check it with a POST
941             my %post;
942             my @mkeys;
943             if ($self->_message_version >= 2
944             && (@mkeys = $self->message->all_parameters)) {
945             # OpenID 2.0: copy *EVERYTHING*, not just signed parameters.
946             # (XXX: Do we need to copy non "openid." parameters as well?
947             # For now, assume if provider is sending them, there is a reason)
948             %post = map {$_ eq 'openid.mode' ? () : ($_, $self->args($_)) } @mkeys;
949             }
950             else {
951             # OpenID 1.1 *OR* legacy client did not provide a proper
952             # enumerator; in the latter case under 2.0 we have no
953             # choice but to send a partial (1.1-style)
954             # check_authentication request and hope for the best.
955              
956             %post = (
957             "openid.assoc_handle" => $assoc_handle,
958             "openid.signed" => $signed,
959             "openid.sig" => $sig64,
960             );
961              
962             if ($self->_message_version >= 2) {
963             $post{'openid.ns'} = OpenID::util::VERSION_2_NAMESPACE();
964             }
965              
966             # and copy in all signed parameters that we don't already have into %post
967             $post{"openid.$_"} = $signed_value{$_}
968             foreach grep {!exists $post{"openid.$_"}} @signed_fields;
969              
970             # if the provider told us our handle as bogus, let's ask in our
971             # check_authentication mode whether that's true
972             if (my $ih = $self->message("invalidate_handle")) {
973             $post{"openid.invalidate_handle"} = $ih;
974             }
975             }
976             $post{"openid.mode"} = "check_authentication";
977              
978             my $req = HTTP::Request->new(POST => $server);
979             $req->header("Content-Type" => "application/x-www-form-urlencoded");
980             $req->content(join("&", map { "$_=" . uri_escape_utf8($post{$_}) } keys %post));
981              
982             my $ua = $self->ua;
983             my $res = $ua->request($req);
984             return $self->_fail("naive_verify_failed_network")
985             unless $res && $res->is_success;
986              
987             my $content = $res->content;
988             my %args = OpenID::util::parse_keyvalue($content);
989              
990             # delete the handle from our cache
991             if (my $ih = $args{'invalidate_handle'}) {
992             Net::OpenID::Association::invalidate_handle($self, $server, $ih);
993             }
994              
995             return $self->_fail("naive_verify_failed_return") unless
996             $args{'is_valid'} eq "true" || # protocol 1.1
997             $args{'lifetime'} > 0; # DEPRECATED protocol 1.0
998             }
999              
1000             $self->_debug("verified identity! = $real_ident");
1001              
1002             # verified!
1003             return Net::OpenID::VerifiedIdentity->new(
1004             claimed_identity => $claimed_identity,
1005             consumer => $self,
1006             signed_fields => \%signed_value,
1007             );
1008             }
1009              
1010             sub supports_consumer_secret { 1; }
1011              
1012             sub _get_consumer_secret {
1013             my Net::OpenID::Consumer $self = shift;
1014             my $time = shift;
1015              
1016             my $ss;
1017             if (ref $self->{consumer_secret} eq "CODE") {
1018             $ss = $self->{consumer_secret};
1019             } elsif ($self->{consumer_secret}) {
1020             $ss = sub { return $self->{consumer_secret}; };
1021             } else {
1022             Carp::croak("You haven't defined a consumer_secret value or subref.\n");
1023             }
1024              
1025             my $sec = $ss->($time);
1026             Carp::croak("Consumer secret too long") if length($sec) > 255;
1027             return $sec;
1028             }
1029              
1030             our $nonce_default_delay = 1200;
1031             our $nonce_default_skew = 300;
1032              
1033             sub _canonicalize_nonce_options {
1034             my Net::OpenID::Consumer $self = shift;
1035             my $o = shift;
1036             my ($no_check,$ignore_time,$lifetime,$window,$start,$skew,$timecop) =
1037             delete @{$o}{qw(no_check ignore_time lifetime window start skew timecop)};
1038             Carp::croak("Unrecognized nonce_options: ".join(',',keys %$o))
1039             if keys %$o;
1040              
1041             return +{ no_check => 1 }
1042             if ($no_check);
1043              
1044             return +{ window => 0,
1045             lifetime => ($lifetime && $lifetime > 0 ? $lifetime : 0),
1046             }
1047             if ($ignore_time);
1048              
1049             $window =
1050             defined($lifetime) ? $lifetime :
1051             $nonce_default_delay + 2*(defined($skew) && $skew > $nonce_default_skew
1052             ? $skew : $nonce_default_skew)
1053             unless (defined($window));
1054              
1055             $lifetime = $window
1056             unless (defined($lifetime));
1057              
1058             $lifetime = 0 if $lifetime < 0;
1059             $window = 0 if $window < 0;
1060              
1061             $skew = $window < 2*$nonce_default_skew ? $window/2 : $nonce_default_skew
1062             unless (defined($skew));
1063              
1064             Carp::croak("Unrecognized nonce_options: ".join(',',keys %$o))
1065             if keys %$o;
1066              
1067             return
1068             +{
1069             window => $window,
1070             lifetime => $lifetime,
1071             skew => $skew,
1072             defined($start) ? (start => $start) : (),
1073             };
1074             }
1075              
1076             # The contract:
1077             # IF the provider adheres to protocol and is properly configured
1078             # which, for our purposes here means
1079             # (1) it sends properly formatted nonces
1080             # that reflect provider clock time and
1081             # (2) provider clock is not skewed from our own by more than
1082             # (the maximum acceptable)
1083             # AND
1084             # we have a cache that can reliably hold onto entries
1085             # for at least seconds
1086             # THEN we must not accept a duplicate nonce.
1087             #
1088             # Preconditions imply that no message with this nonce will be received
1089             # prior to - (i.e., provider clock is running
1090             # maximally fast and there is no transmission delay). If our cache
1091             # start time is prior to this and the lifetime of cache entries is
1092             # long enough, then we can know for certain that it's not a duplicate,
1093             # otherwise we do not and therefore must reject it.
1094             #
1095             # If we detect an instance where preconditions do not hold, there is
1096             # not much we can do: rejecting nonces in this case will not make the
1097             # protocol more secure. As long as the provider's clock is skewed too
1098             # far forward, an attacker will be able to take advantage of it. Best
1099             # we can do is issue warnings, which is the point of 'timecop', but if
1100             # there's no place to send the warnings, then it's a waste of time.
1101             #
1102             sub _nonce_check_succeeds {
1103             my Net::OpenID::Consumer $self = shift;
1104             my ($now, $uri, $nonce) = @_;
1105              
1106             my $o = $self->nonce_options;
1107             my $cache = $self->cache;
1108             return 1
1109             if $o->{no_check} || !$cache;
1110              
1111             my $cache_key = "nonce:$uri:$nonce";
1112              
1113             return $self->_fail('nonce_reused') if ($cache->get($cache_key));
1114             $cache->set($cache_key, 1,
1115             ($o->{lifetime} ? ($now + $o->{lifetime}) : ()));
1116              
1117             return 1
1118             unless $o->{window} || $o->{start};
1119              
1120             # parse RFC3336 timestamp restricted as per 10.1
1121             my ($year,$mon,$day,$hour,$min,$sec) =
1122             $nonce =~ m/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z/
1123             or return $self->_fail('nonce_format');
1124              
1125             # $nonce_time is a lower bound on when the nonce could have been
1126             # received according to our clock
1127             my $nonce_time = eval { timegm($sec,$min,$hour,$day,$mon-1,$year) - $o->{skew} };
1128             return $self->_fail('nonce_format') if $@;
1129              
1130             # nonces from the future indicate misconfigured providers
1131             # that we can do nothing about except give warnings
1132             return !$o->{timecop} || $self->_fail('nonce_future')
1133             if ($now < $nonce_time);
1134              
1135             # the check that matters
1136             return $self->_fail('nonce_stale')
1137             if ($o->{window} && $nonce_time < $now - $o->{window})
1138             || ($o->{start} && $nonce_time < $o->{start});
1139              
1140             # win
1141             return 1;
1142             }
1143              
1144              
1145              
1146             1;
1147             __END__