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