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>