1# LICENSE: You're free to distribute this under the same terms as Perl itself.
2
3use strict;
4use Carp ();
5use Net::OpenID::Common;
6use Net::OpenID::IndirectMessage;
7
8############################################################################
9package Net::OpenID::Server;
10BEGIN {
11  $Net::OpenID::Server::VERSION = '1.09';
12}
13
14use 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
53use Carp;
54use URI;
55use MIME::Base64 ();
56use Digest::SHA qw(sha1 sha1_hex sha256 sha256_hex hmac_sha1 hmac_sha1_hex hmac_sha256 hmac_sha256_hex);
57use Time::Local qw(timegm);
58
59my $OPENID2_NS = qq!http://specs.openid.net/auth/2.0!;
60my $OPENID2_ID_SELECT = qq!http://specs.openid.net/auth/2.0/identifier_select!;
61
62sub 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
102sub get_user     { &_getsetcode; }
103sub get_identity { &_getsetcode; }
104sub is_identity  { &_getsetcode; }
105sub is_trusted   { &_getsetcode; }
106sub handle_request { &_getsetcode; }
107
108sub endpoint_url { &_getset; }
109sub setup_url    { &_getset; }
110sub setup_map    { &_getset; }
111sub compat       { &_getset; }
112
113sub server_secret       { &_getset; }
114sub secret_gen_interval { &_getset; }
115sub 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.
125sub 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                "<html><head><title>OpenID Endpoint</title></head><body>This is an OpenID server endpoint, not a human-readable resource.  For more information, see <a href='http://openid.net/'>http://openid.net/</a>.</body></html>");
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
160sub 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
179sub 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
189sub minimum_version {
190    # TODO: Make this configurable
191    1;
192}
193
194sub _message_mode {
195    my $message = $_[0]->message;
196    return $message ? $message->mode : undef;
197}
198
199sub _message_version {
200    my $message = $_[0]->message;
201    return $message ? $message->protocol_version : undef;
202}
203
204sub 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
216sub 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
324sub _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
418sub _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
438sub _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
446sub _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
457sub _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)
476sub _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
508sub _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
551sub _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
620sub _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
677sub _error_page {
678    my Net::OpenID::Server $self = shift;
679    return $self->_serialized_props({ 'error' => $_[0] });
680}
681
682sub _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
694sub _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 $/; <KF>; };
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
721sub _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
734sub _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
747sub _fail {
748    my Net::OpenID::Server $self = shift;
749    $self->{last_errcode} = shift;
750    $self->{last_errtext} = shift;
751    wantarray ? () : undef;
752}
753
754sub err {
755    my Net::OpenID::Server $self = shift;
756    return undef unless $self->{last_errcode};
757    $self->{last_errcode} . ": " . $self->{last_errtext};
758}
759
760sub errcode {
761    my Net::OpenID::Server $self = shift;
762    $self->{last_errcode};
763}
764
765sub errtext {
766    my Net::OpenID::Server $self = shift;
767    $self->{last_errtext};
768}
769
770# FIXME: duplicated in Net::OpenID::Consumer's VerifiedIdentity
771sub _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
812sub _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__
829
830=head1 NAME
831
832Net::OpenID::Server - Library for building your own OpenID server/provider
833
834=head1 VERSION
835
836version 1.09
837
838=head1 SYNOPSIS
839
840  use Net::OpenID::Server;
841
842  my $nos = Net::OpenID::Server->new(
843    args         => $cgi,
844    get_user     => \&get_user,
845    get_identity => \&get_identity,
846    is_identity  => \&is_identity,
847    is_trusted   => \&is_trusted,
848    endpoint_url => "http://example.com/server.bml",
849    setup_url    => "http://example.com/pass-identity.bml",
850  );
851
852  # From your OpenID server endpoint:
853
854  my ($type, $data) = $nos->handle_page;
855  if ($type eq "redirect") {
856      WebApp::redirect_to($data);
857  } elsif ($type eq "setup") {
858      my %setup_opts = %$data;
859      # ... show them setup page(s), with options from setup_map
860      # it's then your job to redirect them at the end to "return_to"
861      # (or whatever you've named it in setup_map)
862  } else {
863      WebApp::set_content_type($type);
864      WebApp::print($data);
865  }
866
867=head1 DESCRIPTION
868
869This is the Perl API for (the server half of) OpenID, a distributed
870identity system based on proving you own a URL, which is then your
871identity.  More information is available at:
872
873  http://openid.net/
874
875As of version 1.01 this module has support for both OpenID 1.1 and
8762.0. Prior to this, only 1.1 was supported.
877
878=head1 CONSTRUCTOR
879
880=over 4
881
882=item Net::OpenID::Server->B<new>([ %opts ])
883
884You can set anything in the constructor options that there are
885getters/setters methods for below.  That includes: args, get_user,
886is_identity, is_trusted, setup_url, and setup_map.  See below for
887docs.
888
889=back
890
891=head1 METHODS
892
893=over 4
894
895=item ($type, $data) = $nos->B<handle_page>([ %opts ])
896
897Returns a $type and $data, where $type can be:
898
899=over
900
901=item C<redirect>
902
903... in which case you redirect the user (via your web framework's
904redirect functionality) to the URL specified in $data.
905
906=item C<setup>
907
908... in which case you should show the user a page (or redirect them to
909one of your pages) where they can setup trust for the given
910"trust_root" in the hashref in $data, and then redirect them to
911"return_to" at the end.  Note that the parameters in the $data hashref
912are as you named them with setup_map.
913
914=item Some content type
915
916Otherwise, set the content type to $type and print the page out, the
917contents of which are in $data.
918
919=back
920
921The optional %opts may contain:
922
923=over
924
925=item C<redirect_for_setup>
926
927If set to a true value, signals that you don't want to handle the
928C<setup> return type from handle_page, and you'd prefer it just be
929converted to a C<redirect> type to your already-defined C<setup_url>,
930with the arguments from setup_map already appended.
931
932=back
933
934=item $url = $nos->B<signed_return_url>( %opts )
935
936Generates a positive identity assertion URL that you'd redirect a user
937to.  Typically this would be after they've completed your setup_url.
938Once trust has been setup, the C<handle_page> method will redirect you
939to this signed return automatically.
940
941The URL generated is the consumer site's return_to URL, with a signed
942identity included in the GET arguments.  The %opts are:
943
944=over
945
946=item C<identity>
947
948Required.  The identity URL to sign.
949
950=item C<claimed_id>
951
952Optional.  The claimed_id URL to sign.
953
954=item C<return_to>
955
956Required.  The base of the URL being generated.
957
958=item C<assoc_handle>
959
960The association handle to use for the signature.  If blank, dumb
961consumer mode is used, and the library picks the handle.
962
963=item C<trust_root>
964
965Optional.  If present, the C<return_to> URL will be checked to be within
966("under") this trust_root.  If not, the URL returned will be undef.
967
968=item C<ns>
969
970Optional.
971
972=item C<additional_fields>
973
974Optional.  If present, must be a hashref with keys starting with "\w+\.".
975All keys and values will be returned in the response, and signed.  This is
976used for OpenID extensions.
977
978=back
979
980=item $url = $nos->B<cancel_return_url>( %opts )
981
982Generates a cancel notice to the return_to URL, if a user
983declines to share their identity.  %opts are:
984
985=over
986
987=item C<return_to>
988
989Required.  The base of the URL being generated.
990
991=back
992
993=item $nos->B<args>
994
995Can be used in 1 of 3 ways:
996
9971. Setting the way which the Server instances obtains parameters:
998
999$nos->args( $reference )
1000
1001Where $reference is either a HASH ref, CODE ref, Apache $r (for
1002get_args only), Apache::Request $apreq, or CGI.pm $cgi.  If a CODE
1003ref, the subref must return the value given one argument (the
1004parameter to retrieve)
1005
10062. Get a paramater:
1007
1008my $foo = $nos->get_args("foo");
1009
1010When given an unblessed scalar, it retrieves the value.  It croaks if
1011you haven't defined a way to get at the parameters.
1012
10133. Get the getter:
1014
1015my $code = $nos->get_args;
1016
1017Without arguments, returns a subref that returns the value given a
1018parameter name.
1019
1020=item $nos->B<get_user>($code)
1021
1022=item $code = $nos->B<get_user>; $u = $code->();
1023
1024Get/set the subref returning a defined value representing the logged
1025in user, or undef if no user.  The return value (let's call it $u) is
1026not touched.  It's simply given back to your other callbacks
1027(is_identity and is_trusted).
1028
1029=item $nos->B<get_identity>($code)
1030
1031=item $code = $nos->B<get_identity>; $identity = $code->($u, $identity);
1032
1033For OpenID 2.0. Get/set the subref returning a identity. This is called
1034when claimed identity is 'identifier_select'.
1035
1036=item $nos->B<is_identity>($code)
1037
1038=item $code = $nos->B<is_identity>; $code->($u, $identity_url)
1039
1040Get/set the subref which is responsible for returning true if the
1041logged in user $u (which may be undef if user isn't logged in) owns
1042the URL tree given by $identity_url.  Note that if $u is undef, your
1043function should always return 0.  The framework doesn't do that for
1044you so you can do unnecessary work on purpose if you care about
1045exposing information via timing attacks.
1046
1047=item $nos->B<is_trusted>($code)
1048
1049=item $code = $nos->B<is_trusted>; $code->($u, $trust_root, $is_identity)
1050
1051Get/set the subref which is responsible for returning true if the
1052logged in user $u (which may be undef if user isn't logged in) trusts
1053the URL given by $trust_root to know his/her identity.  Note that if
1054either $u is undef, or $is_identity is false (this is the result of
1055your previous is_identity callback), you should return 0.  But your
1056callback is always run so you can avoid timing attacks, if you care.
1057
1058=item $nos->B<server_secret>($scalar)
1059
1060=item $nos->B<server_secret>($code)
1061
1062=item $code = $nos->B<server_secret>; ($secret) = $code->($time);
1063
1064The server secret is used to generate and sign lots of per-consumer
1065secrets, and is never handed out directly.
1066
1067In the simplest (and least secure) form, you configure a static secret
1068value with a scalar.  If you use this method and change the scalar
1069value, all consumers that have cached their per-consumer secrets will
1070start failing, since their secrets no longer work.
1071
1072The recommended usage, however, is to supply a subref that returns a
1073secret based on the provided I<$time>, a unix timestamp.  And if one
1074doesn't exist for that time, create, store and return it (with
1075appropriate locking so you never return different secrets for the same
1076time.)  Your secret can just be random characters, but it's your
1077responsibility to do the locking and storage.  If you want help
1078generating random characters, call C<Net::OpenID::Server::rand_chars($len)>.
1079
1080Your secret may not exceed 255 characters.
1081
1082=item $nos->B<setup_url>($url)
1083
1084=item $url = $nos->B<setup_url>
1085
1086Get/set the user setup URL.  This is the URL the user is told to go to
1087if they're either not logged in, not who they said they were, or trust
1088hasn't been setup.  You use the same URL in all three cases.  Your
1089setup URL may contain existing query parameters.
1090
1091=item $nos->B<endpoint_url>($url)
1092
1093=item $url = $nos->B<endpoint_url>
1094
1095For OpenID 2.0. Get/set the op_endpoint URL.
1096
1097=item $nos->B<setup_map>($hashref)
1098
1099=item $hashref = $nos->B<setup_map>
1100
1101When this module gives a consumer site a user_setup_url from your
1102provided setup_url, it also has to append a number of get parameters
1103onto your setup_url, so your app based at that setup_url knows what it
1104has to setup.  Those keys are named, by default, "trust_root",
1105"return_to", "identity", and "assoc_handle".  If you
1106don't like those parameter names, this $hashref setup_map lets you
1107change one or more of them.  The hashref's keys should be the default
1108values, with values being the parameter names you want.
1109
1110=item Net::OpenID::Server->rand_chars($len)
1111
1112Utility function to return a string of $len random characters.  May be
1113called as package method, object method, or regular function.
1114
1115=item $nos->B<err>
1116
1117Returns the last error, in form "errcode: errtext";
1118
1119=item $nos->B<errcode>
1120
1121Returns the last error code.
1122
1123=item $nos->B<errtext>
1124
1125Returns the last error text.
1126
1127=back
1128
1129=head1 COPYRIGHT
1130
1131This module is Copyright (c) 2005 Brad Fitzpatrick.
1132All rights reserved.
1133
1134You may distribute under the terms of either the GNU General Public
1135License or the Artistic License, as specified in the Perl README file.
1136If you need more liberal licensing terms, please contact the
1137maintainer.
1138
1139=head1 WARRANTY
1140
1141This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND.
1142
1143=head1 MAILING LIST
1144
1145The Net::OpenID family of modules has a mailing list powered
1146by Google Groups. For more information, see
1147http://groups.google.com/group/openid-perl .
1148
1149=head1 SEE ALSO
1150
1151OpenID website:  http://openid.net/
1152
1153=head1 AUTHORS
1154
1155Brad Fitzpatrick <brad@danga.com>