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