File Coverage

blib/lib/Net/OpenID/Server.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


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 1     1   161574 use strict;
  1         3  
  1         111  
4 1     1   7 use Carp ();
  1         2  
  1         23  
5 1     1   870 use Net::OpenID::Common;
  0            
  0            
6             use Net::OpenID::IndirectMessage;
7              
8             ############################################################################
9             package Net::OpenID::Server;
10             BEGIN {
11             $Net::OpenID::Server::VERSION = '1.09';
12             }
13              
14             use fields (
15             'last_errcode', # last error code we got
16             'last_errtext', # last error code we got
17              
18             'get_user', # subref returning a defined value representing the logged in user, or undef if no user.
19             # this return value ($u) is passed to the other subrefs
20              
21             'get_identity', # subref given a ( $u, $identity_url).
22              
23             'is_identity', # subref given a ($u, $identity_url). should return true if $u owns the URL
24             # tree given by $identity_url. not that $u may be undef, if get_user returned undef.
25             # it's up to you if you immediately return 0 on $u or do some work to make the
26             # timing be approximately equal, so you don't reveal if somebody's logged in or not
27              
28             'is_trusted', # subref given a ($u, $trust_root, $is_identity). should return true if $u wants $trust_root
29             # to know about their identity. if you don't care about timing attacks, you can
30             # immediately return 0 if ! $is_identity, as the entire case can't succeed
31             # unless both is_identity and is_trusted pass, and is_identity is called first.
32              
33             'handle_request', # callback to handle a request. If present, get_user, get_identity, is_identity and is_trusted
34             # are all ignored and this single callback is used to replace all of them.
35             'endpoint_url',
36              
37             'setup_url', # setup URL base (optionally with query parameters) where users should go
38             # to login/setup trust/etc.
39              
40             'setup_map', # optional hashref mapping some/all standard keys that would be added to
41             # setup_url to your preferred names.
42              
43             'args', # thing to get args
44             'message', # current IndirectMessage object
45              
46             'server_secret', # subref returning secret given $time
47             'secret_gen_interval',
48             'secret_expire_age',
49              
50             'compat', # version 1.0 compatibility flag (otherwise only sends 1.1 parameters)
51             );
52              
53             use Carp;
54             use URI;
55             use MIME::Base64 ();
56             use Digest::SHA qw(sha1 sha1_hex sha256 sha256_hex hmac_sha1 hmac_sha1_hex hmac_sha256 hmac_sha256_hex);
57             use Time::Local qw(timegm);
58              
59             my $OPENID2_NS = qq!http://specs.openid.net/auth/2.0!;
60             my $OPENID2_ID_SELECT = qq!http://specs.openid.net/auth/2.0/identifier_select!;
61              
62             sub new {
63             my Net::OpenID::Server $self = shift;
64             $self = fields::new( $self ) unless ref $self;
65             my %opts = @_;
66              
67             $self->{last_errcode} = undef;
68             $self->{last_errtext} = undef;
69              
70             if (exists $opts{get_args}) {
71             carp "Option 'get_args' is deprecated, use 'args' instead";
72             $self->args(delete $opts{get_args});
73             }
74             if (exists $opts{post_args}) {
75             carp "Option 'post_args' is deprecated, use 'args' instead";
76             $self->args(delete $opts{post_args});
77             }
78             $self->args(delete $opts{args});
79              
80             $opts{'secret_gen_interval'} ||= 86400;
81             $opts{'secret_expire_age'} ||= 86400 * 14;
82              
83             $opts{'get_identity'} ||= sub { $_[1] };
84              
85             # use compatibility mode until 30 days from July 10, 2005
86             unless (defined $opts{'compat'}) {
87             $opts{'compat'} = time() < 1121052339 + 86400*30 ? 1 : 0;
88             }
89              
90             $self->$_(delete $opts{$_})
91             foreach (qw(
92             get_user get_identity is_identity is_trusted handle_request
93             endpoint_url setup_url setup_map server_secret
94             secret_gen_interval secret_expire_age
95             compat
96             ));
97              
98             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
99             return $self;
100             }
101              
102             sub get_user { &_getsetcode; }
103             sub get_identity { &_getsetcode; }
104             sub is_identity { &_getsetcode; }
105             sub is_trusted { &_getsetcode; }
106             sub handle_request { &_getsetcode; }
107              
108             sub endpoint_url { &_getset; }
109             sub setup_url { &_getset; }
110             sub setup_map { &_getset; }
111             sub compat { &_getset; }
112              
113             sub server_secret { &_getset; }
114             sub secret_gen_interval { &_getset; }
115             sub secret_expire_age { &_getset; }
116              
117              
118             # returns ($content_type, $page), where $content_type can be "redirect"
119             # in which case a temporary redirect should be done to the URL in $page
120             # $content_type can also be "setup", in which case the setup_map variables
121             # are in $page as a hashref, and caller has full control from there.
122             #
123             # returns undef on error, in which case caller should generate an error
124             # page using info in $nos->err.
125             sub handle_page {
126             my Net::OpenID::Server $self = shift;
127             my %opts = @_;
128             my $redirect_for_setup = delete $opts{'redirect_for_setup'};
129             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
130             Carp::croak("handle_page must be called in list context") unless wantarray;
131              
132             my $mode = $self->_message_mode;
133              
134             return $self->_mode_associate
135             if $self->_message_mode eq "associate";
136              
137             return $self->_mode_check_authentication
138             if $self->_message_mode eq "check_authentication";
139              
140             unless ($mode) {
141             return ("text/html",
142             "OpenID EndpointThis is an OpenID server endpoint, not a human-readable resource. For more information, see http://openid.net/.");
143             }
144              
145             return $self->_error_page("Unknown mode")
146             unless $mode =~ /^checkid_(?:immediate|setup)/;
147              
148             return $self->_mode_checkid($mode, $redirect_for_setup);
149             }
150              
151             # given something that can have GET arguments, returns a subref to get them:
152             # Apache
153             # Apache::Request
154             # CGI
155             # HASH of get args
156             # CODE returning get arg, given key
157              
158             # ...
159              
160             sub args {
161             my Net::OpenID::Server $self = shift;
162              
163             if (my $what = shift) {
164             unless (ref $what) {
165             return $self->{args} ? $self->{args}->($what) : Carp::croak("No args defined");
166             }
167             else {
168             Carp::croak("Too many parameters") if @_;
169             my $message = Net::OpenID::IndirectMessage->new($what, (
170             minimum_version => $self->minimum_version,
171             ));
172             $self->{message} = $message;
173             $self->{args} = $message ? $message->getter : sub { undef };
174             }
175             }
176             $self->{args};
177             }
178              
179             sub message {
180             my Net::OpenID::Server $self = shift;
181             if (my $key = shift) {
182             return $self->{message} ? $self->{message}->get($key) : undef;
183             }
184             else {
185             return $self->{message};
186             }
187             }
188              
189             sub minimum_version {
190             # TODO: Make this configurable
191             1;
192             }
193              
194             sub _message_mode {
195             my $message = $_[0]->message;
196             return $message ? $message->mode : undef;
197             }
198              
199             sub _message_version {
200             my $message = $_[0]->message;
201             return $message ? $message->protocol_version : undef;
202             }
203              
204             sub cancel_return_url {
205             my Net::OpenID::Server $self = shift;
206              
207             my %opts = @_;
208             my $return_to = delete $opts{'return_to'};
209             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
210              
211             my $ret_url = $return_to;
212             OpenID::util::push_url_arg(\$ret_url, "openid.mode" => "cancel");
213             return $ret_url;
214             }
215              
216             sub signed_return_url {
217             my Net::OpenID::Server $self = shift;
218             my %opts = @_;
219             my $identity = delete $opts{'identity'};
220             my $claimed_id = delete $opts{'claimed_id'};
221             my $return_to = delete $opts{'return_to'};
222             my $assoc_handle = delete $opts{'assoc_handle'};
223             my $assoc_type = delete $opts{'assoc_type'} || 'HMAC-SHA1';
224             my $ns = delete $opts{'ns'};
225             my $extra_fields = delete $opts{'additional_fields'} || {};
226              
227             # verify the trust_root and realm, if provided
228             if (my $realm = delete $opts{'realm'}) {
229             return undef unless _url_is_under($realm, $return_to);
230             delete $opts{'trust_root'};
231             } elsif (my $trust_root = delete $opts{'trust_root'}) {
232             return undef unless _url_is_under($trust_root, $return_to);
233             }
234             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
235              
236             my $ret_url = $return_to;
237              
238             my $c_sec;
239             my $invalid_handle;
240              
241             if ($assoc_handle) {
242             $c_sec = $self->_secret_of_handle($assoc_handle, type=>$assoc_type);
243              
244             # tell the consumer that their provided handle is bogus
245             # (or we forgot it) and that they should stop using it
246             $invalid_handle = $assoc_handle unless $c_sec;
247             }
248              
249             unless ($c_sec) {
250             # dumb consumer mode
251             ($assoc_handle, $c_sec, undef) = $self->_generate_association(type => $assoc_type,
252             dumb => 1);
253             }
254              
255             $claimed_id ||= $identity;
256             $claimed_id = $identity if $claimed_id eq $OPENID2_ID_SELECT;
257             my @sign = qw(mode claimed_id identity op_endpoint return_to response_nonce assoc_handle assoc_type);
258              
259             my $now = time();
260             my %arg = (
261             mode => "id_res",
262             identity => $identity,
263             claimed_id => $claimed_id,
264             return_to => $return_to,
265             assoc_handle => $assoc_handle,
266             assoc_type => $assoc_type,
267             response_nonce => OpenID::util::time_to_w3c($now) . _rand_chars(6),
268             );
269             $arg{'op_endpoint'} = $self->endpoint_url if $self->endpoint_url && $ns eq $OPENID2_NS;
270             $arg{'ns'} = $ns if $ns;
271              
272             # compatibility mode with version 1.0 of the protocol which still
273             # had absolute dates
274             if ($self->{compat}) {
275             $arg{issued} = OpenID::util::time_to_w3c($now);
276             $arg{valid_to} = OpenID::util::time_to_w3c($now + 3600);
277             push @sign, "issued", "valid_to";
278             }
279              
280             # add in the additional fields
281             foreach my $k (keys %{ $extra_fields }) {
282             die "Invalid extra field: $k" unless
283             $k =~ /^\w+\./;
284             $arg{$k} = $extra_fields->{$k};
285             push @sign, $k;
286             }
287              
288             # since signing of empty fields is not well defined,
289             # remove such fields from the list of fields to be signed
290             @sign = grep { defined $arg{$_} && $arg{$_} ne '' } @sign;
291             $arg{signed} = join(",", @sign);
292              
293             my @arg; # arguments we'll append to the URL
294             my $token_contents = "";
295             foreach my $f (@sign) {
296             $token_contents .= "$f:$arg{$f}\n";
297             push @arg, "openid.$f" => $arg{$f};
298             delete $arg{$f};
299             }
300              
301             # include the arguments we didn't sign in the URL
302             push @arg, map { ( "openid.$_" => $arg{$_} ) } sort keys %arg;
303              
304             # include (unsigned) the handle we're telling the consumer to invalidate
305             if ($invalid_handle) {
306             push @arg, "openid.invalidate_handle" => $invalid_handle;
307             }
308              
309             # finally include the signature
310             if ($assoc_type eq 'HMAC-SHA1') {
311             push @arg, "openid.sig" => OpenID::util::b64(hmac_sha1($token_contents, $c_sec));
312             }
313             elsif ($assoc_type eq 'HMAC-SHA256') {
314             push @arg, "openid.sig" => OpenID::util::b64(hmac_sha256($token_contents, $c_sec));
315             }
316             else {
317             die "Unknown assoc_type $assoc_type";
318             }
319              
320             OpenID::util::push_url_arg(\$ret_url, @arg);
321             return $ret_url;
322             }
323              
324             sub _mode_checkid {
325             my Net::OpenID::Server $self = shift;
326             my ($mode, $redirect_for_setup) = @_;
327              
328             my $return_to = $self->args("openid.return_to");
329             return $self->_fail("no_return_to") unless $return_to =~ m!^https?://!;
330              
331             my $trust_root = $self->args("openid.trust_root") || $return_to;
332             $trust_root = $self->args("openid.realm") if $self->args('openid.ns') eq $OPENID2_NS;
333             return $self->_fail("invalid_trust_root") unless _url_is_under($trust_root, $return_to);
334              
335             my $identity = $self->args("openid.identity");
336              
337             # chop off the query string, in case our trust_root came from the return_to URL
338             $trust_root =~ s/\?.*//;
339              
340             my $is_identity = 0;
341             my $is_trusted = 0;
342             if (0 && $self->{handle_request}) {
343              
344              
345             }
346             else {
347             my $u = $self->_proxy("get_user");
348             if ( $self->args('openid.ns') eq $OPENID2_NS && $identity eq $OPENID2_ID_SELECT ) {
349             $identity = $self->_proxy("get_identity", $u, $identity );
350             }
351             $is_identity = $self->_proxy("is_identity", $u, $identity);
352             $is_trusted = $self->_proxy("is_trusted", $u, $trust_root, $is_identity);
353             }
354              
355             # assertion path:
356             if ($is_identity && $is_trusted) {
357             my $ret_url = $self->signed_return_url(
358             identity => $identity,
359             claimed_id => $self->args('openid.claimed_id'),
360             return_to => $return_to,
361             assoc_handle => $self->args("openid.assoc_handle"),
362             assoc_type => $self->args("openid.assoc_type"),
363             ns => $self->args('openid.ns'),
364             );
365             return ("redirect", $ret_url);
366             }
367              
368             # assertion could not be made, so user requires setup (login/trust.. something)
369             # two ways that can happen: caller might have asked us for an immediate return
370             # with a setup URL (the default), or explictly said that we're in control of
371             # the user-agent's full window, and we can do whatever we want with them now.
372             my %setup_args = (
373             $self->_setup_map("trust_root"), $trust_root,
374             $self->_setup_map("realm"), $trust_root,
375             $self->_setup_map("return_to"), $return_to,
376             $self->_setup_map("identity"), $identity,
377             );
378             $setup_args{$self->_setup_map('ns')} = $self->args('openid.ns') if $self->args('openid.ns');
379              
380             if ( $self->args("openid.assoc_handle") ) {
381             $setup_args{ $self->_setup_map("assoc_handle") } =
382             $self->args("openid.assoc_handle");
383             $setup_args{ $self->_setup_map("assoc_type") } =
384             $self->_determine_assoc_type_from_assoc_handle(
385             $self->args("openid.assoc_handle") );
386             }
387              
388             my $setup_url = $self->{setup_url} or Carp::croak("No setup_url defined.");
389             OpenID::util::push_url_arg(\$setup_url, %setup_args);
390              
391             if ($mode eq "checkid_immediate") {
392             my $ret_url = $return_to;
393             OpenID::util::push_url_arg(\$setup_url, 'openid.mode'=>'checkid_setup');
394             OpenID::util::push_url_arg(\$setup_url, 'openid.claimed_id'=>$identity);
395             if ($self->args('openid.ns') eq $OPENID2_NS) {
396             OpenID::util::push_url_arg(\$ret_url, "openid.ns", $self->args('openid.ns'));
397             OpenID::util::push_url_arg(\$ret_url, "openid.mode", "setup_needed");
398             } else {
399             OpenID::util::push_url_arg(\$ret_url, "openid.mode", "id_res");
400             }
401             # We send this even in the 2.0 case -- despite what the spec says --
402             # because several consumer implementations, including Net::OpenID::Consumer
403             # at this time, depend on it.
404             OpenID::util::push_url_arg(\$ret_url, "openid.user_setup_url", $setup_url);
405             return ("redirect", $ret_url);
406             } else {
407             # the "checkid_setup" mode, where we take control of the user-agent
408             # and return to their return_to URL later.
409              
410             if ($redirect_for_setup) {
411             return ("redirect", $setup_url);
412             } else {
413             return ("setup", \%setup_args);
414             }
415             }
416             }
417              
418             sub _determine_assoc_type_from_assoc_handle {
419             my ($self, $assoc_handle)=@_;
420              
421             my $assoc_type=$self->args("openid.assoc_type");
422             return $assoc_type if ($assoc_type); # set? Just return it.
423              
424             if ($assoc_handle) {
425             my (undef, undef, $hmac_part)=split /:/, $assoc_handle, 3;
426             my $len=length($hmac_part); # see _generate_association
427             if ($len==16) {
428             $assoc_type='HMAC-SHA256';
429             }
430             elsif ($len==10) {
431             $assoc_type='HMAC-SHA1';
432             }
433             }
434              
435             return $assoc_type;
436             }
437              
438             sub _setup_map {
439             my Net::OpenID::Server $self = shift;
440             my $key = shift;
441             Carp::croak("Too many parameters") if @_;
442             return $key unless ref $self->{setup_map} eq "HASH" && $self->{setup_map}{$key};
443             return $self->{setup_map}{$key};
444             }
445              
446             sub _proxy {
447             my Net::OpenID::Server $self = shift;
448             my $meth = shift;
449              
450             my $getter = $self->{$meth};
451             Carp::croak("You haven't defined a subref for '$meth'")
452             unless ref $getter eq "CODE";
453              
454             return $getter->(@_);
455             }
456              
457             sub _get_server_secret {
458             my Net::OpenID::Server $self = shift;
459             my $time = shift;
460              
461             my $ss;
462             if (ref $self->{server_secret} eq "CODE") {
463             $ss = $self->{server_secret};
464             } elsif ($self->{server_secret}) {
465             $ss = sub { return $self->{server_secret}; };
466             } else {
467             Carp::croak("You haven't defined a server_secret value or subref defined.\n");
468             }
469              
470             my $sec = $ss->($time);
471             Carp::croak("Server secret too long") if length($sec) > 255;
472             return $sec;
473             }
474              
475             # returns ($assoc_handle, $secret, $expires)
476             sub _generate_association {
477             my Net::OpenID::Server $self = shift;
478             my %opts = @_;
479             my $type = delete $opts{type};
480             my $dumb = delete $opts{dumb} || 0;
481             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
482             die unless $type =~ /^HMAC-SHA(1|256)$/;
483              
484             my $now = time();
485             my $sec_time = $now - ($now % $self->secret_gen_interval);
486              
487             my $s_sec = $self->_get_server_secret($sec_time)
488             or Carp::croak("server_secret didn't return a secret given what should've been a valid time ($sec_time)\n");
489              
490             my $nonce = _rand_chars(20);
491             $nonce = "STLS.$nonce" if $dumb; # flag nonce as stateless
492              
493             my $handle = "$now:$nonce";
494             if ($type eq 'HMAC-SHA1') {
495             $handle .= ":" . substr(hmac_sha1_hex($handle, $s_sec), 0, 10);
496             }
497             elsif ($type eq 'HMAC-SHA256') {
498             $handle .= ":" . substr(hmac_sha256_hex($handle, $s_sec), 0, 16);
499             }
500              
501             my $c_sec = $self->_secret_of_handle($handle, dumb => $dumb, type=>$type)
502             or return ();
503              
504             my $expires = $sec_time + $self->secret_expire_age;
505             return ($handle, $c_sec, $expires);
506             }
507              
508             sub _secret_of_handle {
509             my Net::OpenID::Server $self = shift;
510             my ($handle, %opts) = @_;
511              
512             my $dumb_mode = delete $opts{'dumb'} || 0;
513             my $no_verify = delete $opts{'no_verify'} || 0;
514             my $type = delete $opts{'type'} || 'HMAC-SHA1';
515             my %hmac_functions_hex=(
516             'HMAC-SHA1' =>\&hmac_sha1_hex,
517             'HMAC-SHA256'=>\&hmac_sha256_hex,
518             );
519             my %hmac_functions=(
520             'HMAC-SHA1' =>\&hmac_sha1,
521             'HMAC-SHA256'=>\&hmac_sha256,
522             );
523             my %nonce_80_lengths=(
524             'HMAC-SHA1'=>10,
525             'HMAC-SHA256'=>16,
526             );
527             my $nonce_80_len=$nonce_80_lengths{$type};
528             my $hmac_function_hex=$hmac_functions_hex{$type} || Carp::croak "No function for $type";
529             my $hmac_function=$hmac_functions{$type} || Carp::croak "No function for $type";
530             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
531              
532             my ($time, $nonce, $nonce_sig80) = split(/:/, $handle);
533             return unless $time =~ /^\d+$/ && $nonce && $nonce_sig80;
534              
535             # check_authentication mode only verifies signatures made with
536             # dumb (stateless == STLS) handles, so if that caller requests it,
537             # don't return the secrets here of non-stateless handles
538             return if $dumb_mode && $nonce !~ /^STLS\./;
539              
540             my $sec_time = $time - ($time % $self->secret_gen_interval);
541             my $s_sec = $self->_get_server_secret($sec_time) or return;
542              
543             length($nonce) == ($dumb_mode ? 25 : 20) or return;
544             length($nonce_sig80) == $nonce_80_len or return;
545              
546             return unless $no_verify || $nonce_sig80 eq substr($hmac_function_hex->("$time:$nonce", $s_sec), 0, $nonce_80_len);
547              
548             return $hmac_function->($handle, $s_sec);
549             }
550              
551             sub _mode_associate {
552             my Net::OpenID::Server $self = shift;
553              
554             my $now = time();
555             my %prop;
556              
557             my $assoc_type = $self->message('assoc_type') || "HMAC-SHA1";
558              
559             if ($self->message('ns') eq $OPENID2_NS &&
560             ($self->message('assoc_type') ne $assoc_type ||
561             $self->message('session_type') ne 'DH-SHA1')) {
562              
563             $prop{'ns'} = $self->message('ns') if $self->message('ns');
564             $prop{'error_code'} = "unsupported-type";
565             $prop{'error'} = "This server support $assoc_type only.";
566             $prop{'assoc_type'} = $assoc_type;
567             $prop{'session_type'} = "DH-SHA1";
568              
569             return $self->_serialized_props(\%prop);
570             }
571              
572             my ($assoc_handle, $secret, $expires) =
573             $self->_generate_association(type => $assoc_type);
574              
575             # make absolute form of expires
576             my $exp_abs = $expires > 1000000000 ? $expires : $expires + $now;
577              
578             # make relative form of expires
579             my $exp_rel = $exp_abs - $now;
580              
581             $prop{'ns'} = $self->args('openid.ns') if $self->args('openid.ns');
582             $prop{'assoc_type'} = $assoc_type;
583             $prop{'assoc_handle'} = $assoc_handle;
584             $prop{'assoc_type'} = $assoc_type;
585             $prop{'expires_in'} = $exp_rel;
586              
587             if ($self->{compat}) {
588             $prop{'expiry'} = OpenID::util::time_to_w3c($exp_abs);
589             $prop{'issued'} = OpenID::util::time_to_w3c($now);
590             }
591              
592             if ($self->args("openid.session_type") =~ /^DH-SHA(1|256)$/) {
593              
594             my $p = OpenID::util::arg2int($self->args("openid.dh_modulus"));
595             my $g = OpenID::util::arg2int($self->args("openid.dh_gen"));
596             my $cpub = OpenID::util::arg2int($self->args("openid.dh_consumer_public"));
597              
598             my $dh = OpenID::util::get_dh($p, $g);
599             return $self->_error_page("invalid dh params p=$p, g=$g, cpub=$cpub")
600             unless $dh and $cpub;
601              
602             my $dh_sec = $dh->compute_secret($cpub);
603              
604             $prop{'dh_server_public'} = OpenID::util::int2arg($dh->pub_key);
605             $prop{'session_type'} = $self->message("session_type");
606             if ($self->args("openid.session_type") eq 'DH-SHA1') {
607             $prop{'enc_mac_key'} = OpenID::util::b64($secret ^ sha1(OpenID::util::int2bytes($dh_sec)));
608             }
609             elsif ($self->args("openid.session_type") eq 'DH-SHA256') {
610             $prop{'enc_mac_key'} = OpenID::util::b64($secret ^ sha256(OpenID::util::int2bytes($dh_sec)));
611             }
612              
613             } else {
614             $prop{'mac_key'} = OpenID::util::b64($secret);
615             }
616              
617             return $self->_serialized_props(\%prop);
618             }
619              
620             sub _mode_check_authentication {
621             my Net::OpenID::Server $self = shift;
622              
623             my $signed = $self->args("openid.signed") || "";
624             my $token = "";
625             foreach my $param (split(/,/, $signed)) {
626             next unless $param =~ /^[\w\.]+$/;
627             my $val = $param eq "mode" ? "id_res" : $self->args("openid.$param");
628             next unless defined $val;
629             next if $val =~ /\n/;
630             $token .= "$param:$val\n";
631             }
632              
633             my $sig = $self->args("openid.sig");
634             my $ahandle = $self->args("openid.assoc_handle")
635             or return $self->_error_page("no_assoc_handle");
636              
637             my $c_sec = $self->_secret_of_handle($ahandle, dumb => 1)
638             or return $self->_error_page("bad_handle");
639              
640             my $assoc_type = $self->args('openid.assoc_type') || 'HMAC-SHA1';
641              
642             my $good_sig;
643             if ($assoc_type eq 'HMAC-SHA1') {
644             $good_sig = OpenID::util::b64(hmac_sha1($token, $c_sec));
645             }
646             elsif ($assoc_type eq 'HMAC-SHA256') {
647             $good_sig = OpenID::util::b64(hmac_sha256($token, $c_sec));
648             }
649             else {
650             die "Unknown assoc_type $assoc_type";
651             }
652              
653             my $is_valid = OpenID::util::timing_indep_eq($sig, $good_sig);
654              
655             my $ret = {
656             is_valid => $is_valid ? "true" : "false",
657             };
658             $ret->{'ns'} = $self->args('openid.ns') if $self->args('openid.ns');
659              
660             if ($self->{compat}) {
661             $ret->{lifetime} = 3600;
662             $ret->{WARNING} =
663             "The lifetime parameter is deprecated and will " .
664             "soon be removed. Use is_valid instead. " .
665             "See openid.net/specs.bml.";
666             }
667              
668             # tell them if a handle they asked about is invalid, too
669             if (my $ih = $self->args("openid.invalidate_handle")) {
670             $c_sec = $self->_secret_of_handle($ih);
671             $ret->{"invalidate_handle"} = $ih unless $c_sec;
672             }
673              
674             return $self->_serialized_props($ret);
675             }
676              
677             sub _error_page {
678             my Net::OpenID::Server $self = shift;
679             return $self->_serialized_props({ 'error' => $_[0] });
680             }
681              
682             sub _serialized_props {
683             my Net::OpenID::Server $self = shift;
684             my $props = shift;
685              
686             my $body = "";
687             foreach (sort keys %$props) {
688             $body .= "$_:$props->{$_}\n";
689             }
690              
691             return ("text/plain", $body);
692             }
693              
694             sub _get_key_contents {
695             my Net::OpenID::Server $self = shift;
696             my $key = shift;
697             Carp::croak("Too many parameters") if @_;
698             Carp::croak("Unknown key type") unless $key =~ /^public|private$/;
699              
700             my $mval = $self->{"${key}_key"};
701             my $contents;
702              
703             if (ref $mval eq "CODE") {
704             $contents = $mval->();
705             } elsif ($mval !~ /\n/ && -f $mval) {
706             local *KF;
707             return $self->_fail("key_open_failure", "Couldn't open key file for reading")
708             unless open(KF, $mval);
709             $contents = do { local $/; ; };
710             close KF;
711             } else {
712             $contents = $mval;
713             }
714              
715             return $self->_fail("invalid_key", "$key file not in correct format")
716             unless $contents =~ /\-\-\-\-BEGIN/ && $contents =~ /\-\-\-\-END/;
717             return $contents;
718             }
719              
720              
721             sub _getset {
722             my Net::OpenID::Server $self = shift;
723             my $param = (caller(1))[3];
724             $param =~ s/.+:://;
725              
726             if (@_) {
727             my $val = shift;
728             Carp::croak("Too many parameters") if @_;
729             $self->{$param} = $val;
730             }
731             return $self->{$param};
732             }
733              
734             sub _getsetcode {
735             my Net::OpenID::Server $self = shift;
736             my $param = (caller(1))[3];
737             $param =~ s/.+:://;
738              
739             if (my $code = shift) {
740             Carp::croak("Too many parameters") if @_;
741             Carp::croak("Expected CODE reference") unless ref $code eq "CODE";
742             $self->{$param} = $code;
743             }
744             return $self->{$param};
745             }
746              
747             sub _fail {
748             my Net::OpenID::Server $self = shift;
749             $self->{last_errcode} = shift;
750             $self->{last_errtext} = shift;
751             wantarray ? () : undef;
752             }
753              
754             sub err {
755             my Net::OpenID::Server $self = shift;
756             return undef unless $self->{last_errcode};
757             $self->{last_errcode} . ": " . $self->{last_errtext};
758             }
759              
760             sub errcode {
761             my Net::OpenID::Server $self = shift;
762             $self->{last_errcode};
763             }
764              
765             sub errtext {
766             my Net::OpenID::Server $self = shift;
767             $self->{last_errtext};
768             }
769              
770             # FIXME: duplicated in Net::OpenID::Consumer's VerifiedIdentity
771             sub _url_is_under {
772             my ($root, $test, $err_ref) = @_;
773              
774             my $err = sub {
775             $$err_ref = shift if $err_ref;
776             return undef;
777             };
778              
779             my $ru = URI->new($root);
780             return $err->("invalid root scheme") unless $ru->scheme =~ /^https?$/;
781             my $tu = URI->new($test);
782             return $err->("invalid test scheme") unless $tu->scheme =~ /^https?$/;
783             return $err->("schemes don't match") unless $ru->scheme eq $tu->scheme;
784             return $err->("ports don't match") unless $ru->port == $tu->port;
785              
786             # check hostnames
787             my $ru_host = $ru->host;
788             my $tu_host = $tu->host;
789             my $wildcard_host = 0;
790             if ($ru_host =~ s!^\*\.!!) {
791             $wildcard_host = 1;
792             }
793             unless ($ru_host eq $tu_host) {
794             if ($wildcard_host) {
795             return $err->("host names don't match") unless
796             $tu_host =~ /\.\Q$ru_host\E$/;
797             } else {
798             return $err->("host names don't match");
799             }
800             }
801              
802             # check paths
803             my $ru_path = $ru->path || "/";
804             my $tu_path = $tu->path || "/";
805             $ru_path .= "/" unless $ru_path =~ m!/$!;
806             $tu_path .= "/" unless $tu_path =~ m!/$!;
807             return $err->("path not a subpath") unless $tu_path =~ m!^\Q$ru_path\E!;
808              
809             return 1;
810             }
811              
812             sub _rand_chars
813             {
814             shift if @_ == 2; # shift off classname/obj, if called as method
815             my $length = shift;
816              
817             my $chal = "";
818             my $digits = "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0123456789";
819             for (1..$length) {
820             $chal .= substr($digits, int(rand(62)), 1);
821             }
822             return $chal;
823             }
824              
825             # also a public interface:
826             *rand_chars = \&_rand_chars;
827              
828             __END__