1my $successResponseClass = "Net::OpenID::JanRain::Consumer::SuccessResponse"; 2my $failureResponseClass = "Net::OpenID::JanRain::Consumer::FailureResponse"; 3my $cancelResponseClass = "Net::OpenID::JanRain::Consumer::CancelResponse"; 4my $setupNeededResponseClass = "Net::OpenID::JanRain::Consumer::SetupNeededResponse"; 5 6package Net::OpenID::JanRain::Consumer; 7 8=head1 OVERVIEW 9 10The OpenID identity verification process most commonly uses the 11following steps, as visible to the user of this library: 12 13=over 14 15=item 1. 16 17The user enters their OpenID into a field on the consumer's 18site, and hits a login button. 19 20=item 2. 21 22The consumer site discovers the user's OpenID server using 23the YADIS protocol. 24 25=item 3. 26 27The consumer site sends the browser a redirect to the 28identity server. This is the authentication request as 29described in the OpenID specification. 30 31=item 4. 32 33The identity server's site sends the browser a redirect 34back to the consumer site. This redirect contains the 35server's response to the authentication request. 36 37=back 38 39The most important part of the flow to note is the consumer's site 40must handle two separate HTTP requests in order to perform the 41full identity check. 42 43=head2 LIBRARY DESIGN 44 45This consumer library is designed with that flow in mind. The 46goal is to make it as easy as possible to perform the above steps 47securely. 48 49At a high level, there are two important parts in the consumer 50library. The first important part is this module, which contains 51the interface to actually use this library. The second is the 52L<Net::OpenID::JanRain::Stores|Net::OpenID::JanRain::Stores> module, 53which describes the 54interface to use if you need to create a custom method for storing 55the state this library needs to maintain between requests. 56 57In general, the second part is less important for users of the 58library to know about, as several implementations are provided 59which cover a wide variety of situations in which consumers may 60use the library. 61 62This module contains a class, C<Net::OpenID::JanRain::Consumer>, with methods 63corresponding to the actions necessary in each of steps 2, 3, and 644 described in the overview. Use of this library should be as easy 65as creating a Consumer instance and calling the methods 66appropriate for the action the site wants to take. 67 68=head2 STORES AND DUMB MODE 69 70OpenID is a protocol that works best when the consumer site is 71able to store some state. This is the normal mode of operation 72for the protocol, and is sometimes referred to as smart mode. 73There is also a fallback mode, known as dumb mode, which is 74available when the consumer site is not able to store state. This 75mode should be avoided when possible, as it leaves the 76implementation more vulnerable to replay attacks. 77 78The mode the library works in for normal operation is determined 79by the store that it is given. The store is an abstraction that 80handles the data that the consumer needs to manage between http 81requests in order to operate efficiently and securely. 82 83Several store implementation are provided, and the interface is 84fully documented so that custom stores can be used as well. See 85L<Net::OpenID::JanRain::Stores> for more 86information on the interface for stores. The implementations that 87are provided allow the consumer site to store the necessary data 88in several different ways, including several SQL databases and 89normal files on disk. 90 91There is an additional concrete store provided that puts the 92system in dumb mode. This is not recommended, as it removes the 93library's ability to stop replay attacks reliably. It still uses 94time-based checking to make replay attacks only possible within a 95small window, but they remain possible within that window. This 96store should only be used if the consumer site has no way to 97retain data between requests at all. 98 99=head2 IMMEDIATE MODE 100 101In the flow described above, the user may need to confirm to the 102identity server that it's ok to authorize his or her identity. 103The server may draw pages asking for information from the user 104before it redirects the browser back to the consumer's site. This 105is generally transparent to the consumer site, so it is typically 106ignored as an implementation detail. 107 108There can be times, however, where the consumer site wants to get 109a response immediately. When this is the case, the consumer can 110put the library in immediate mode. In immediate mode, there is an 111extra response possible from the server, which is essentially the 112server reporting that it doesn't have enough information to answer 113the question yet. In addition to saying that, the identity server 114provides a URL to which the user can be sent to provide the needed 115information and let the server finish handling the original 116request. 117 118=head2 USING THIS LIBRARY 119 120Integrating this library into an application is usually a 121relatively straightforward process. The process should basically 122follow this plan: 123 124Add an OpenID login field somewhere on your site. When an OpenID 125is entered in that field and the form is submitted, it should make 126a request to the your site which includes that OpenID URL. 127 128First, the application should instantiate the 129C<Net::OpenID::JanRain::Consumer> class 130using the store of choice. You may also pass a L<CGI::Session|CGI::Session> object 131to the constructor, which will store user transaction data. 132 133Next, the application should call the 'begin' method on the 134C<Consumer> instance. This method takes the OpenID URL. The 135L</begin> method returns an L</Net::OpenID::JanRain::Consumer::AuthRequest> 136object. 137 138Next, the application should call the 139L</redirectURL> method on the 140L</Net::OpenID::JanRain::Consumer::AuthRequest> object. 141The parameter C<return_to> is the URL 142that the OpenID server will send the user back to after attempting 143to verify his or her identity. The C<trust_root> parameter is the 144URL (or URL pattern) that identifies your web site to the user 145when he or she is authorizing it. Send a redirect to the 146resulting URL to the user's browser. 147 148That's the first half of the authentication process. The second 149half of the process is done after the user's ID server sends the 150user's browser a redirect back to your site to complete their 151login. 152 153When that happens, the user will contact your site at the URL 154given as the C<return_to> URL to the 155L</redirectURL> call made 156above. The request will have several query parameters added to 157the URL by the identity server as the information necessary to 158finish the request. 159 160Get an C<Consumer> instance, and call its 161L</complete> method, passing in all the 162received query arguments. 163 164If that call is successful, the user is authenticated. 165 166=cut 167 168use Carp; 169use URI; 170use Net::OpenID::JanRain::Util qw( findAgent normalizeUrl ); 171use Net::Yadis; 172 173my $OPENID_NS = "http://openid.net/xmlns/1.0"; 174my $OPENID_SERVICE_TYPE = "http://openid\\.net/signon/1\\.[012]"; 175my $TOKEN_KEY = '_openid_consumer_token'; 176my $ENDPOINTS_KEY = '_openid_consumer_endpoints'; 177 178=head1 Methods of Net::OpenID::JanRain::Consumer 179 180=head2 new 181 182 $consumer = Net::OpenID::JanRain::Consumer->new($session, $store); 183 184=head3 arguments 185 186=over 187 188=item session 189 190Must be an instance of L<CGI::Session|CGI::Session>. Used to store user-specific 191transaction data, including a list of openid services found in the 192user's Yadis file, allowing fallback if the primary service is down. 193Currently required, but may be made optional. 194 195=item store 196 197Must be an instance of L<Net::OpenID::JanRain::Stores|Net::OpenID::JanRain::Stores>, and is used 198to store association and nonce data. 199 200=back 201 202=cut 203 204sub new { 205 my $caller = shift; 206 my $session = shift || die "OpenID consumer needs a session"; 207 my $store = shift || die "OpenID consumer needs a store"; 208 my $class = ref($caller) || $caller; 209 210 my $self = { 211 session => $session, 212 consumer => Net::OpenID::JanRain::GenericConsumer->new($store), 213 }; 214 215 bless ($self, $class); 216} 217 218=head2 begin 219 220=head3 Argument 221 222=over 223 224=item user_url 225 226The url entered by a user, as on a web form. This url will be canonicalized, 227prepending C<http://> if it is not present. 228 229=back 230 231=head3 Returns 232 233Returns an instance of either 234L</Net::OpenID::JanRain::Consumer::FailureResponse> (upon failure) 235or L</Net::OpenID::JanRain::Consumer::AuthRequest> if the initial steps 236of the protocol succeeded. 237 238=cut 239 240sub begin { 241 my ($self, $user_url) = @_; 242 243 my $endpointlist = $self->{session}->param($ENDPOINTS_KEY); 244 unless($endpointlist) { 245 my $openid_url = normalizeUrl($user_url); 246 my $foo; 247 ($foo, $endpointlist) = discover($openid_url); 248 if (defined $endpointlist) { 249 $openid_url = $foo; 250 } 251 else { 252 return $failureResponseClass->new($openid_url, $foo); 253 } 254 } 255 256 my $endpoint = shift @$endpointlist; 257 258 if(@$endpointlist == 0) { 259 $self->{session}->clear([$ENDPOINTS_KEY]); 260 } 261 else { 262 $self->{session}->param($ENDPOINTS_KEY, $endpointlist); 263 } 264 return $self->beginWithoutDiscovery($endpoint); 265} 266 267sub discover { 268 my $uri = shift or carp "Cannot discover nothing"; 269 my $filter = shift; 270 271 my $yadis; 272 eval { 273 $yadis = Net::Yadis->discover($uri); 274 }; 275 if ($@) { 276 # openid_log("Yadis discovery failed: $@"); 277 my ($one, $two) = old_school_discover($uri); 278 if(defined($two)) { 279 return ($one, $two); 280 } 281 else { 282 return ($@, undef); 283 } 284 } 285 286 my $id_url = $yadis->url; 287 288 $filter = sub { 289 my $service = shift; 290 291 $service->is_type($OPENID_SERVICE_TYPE) || return undef; 292 293 my $endpoint = Net::OpenID::JanRain::Consumer::ServiceEndpoint->new; 294 295 $endpoint->{delegate} = $service->findTag('Delegate', $OPENID_NS); 296 $endpoint->{server_url} = $service->uri; 297 $endpoint->{type_uris} = [$service->types]; 298 299 return $endpoint; 300 } unless defined($filter); 301 302 my @openid_endpoints = $yadis->filter_services($filter); 303 304 # for convenience, although it's weird to do this 305 foreach $endpoint (@openid_endpoints) { 306 $endpoint->{identity_url} = $id_url; 307 } 308 309 return ("Found no OpenID services", undef) unless @openid_endpoints; 310 311 return ($id_url, \@openid_endpoints); 312} 313 314sub old_school_discover { 315 my $uri = shift; 316 317 my $ua = findAgent()->new; 318 my $resp = $ua->get($uri); 319 return ("Could not fetch $uri", undef) unless $resp->is_success; 320 my $id_url = $resp->base; # follow redirects 321 my $html = $resp->content; 322 323 my $endpoint = Net::OpenID::JanRain::Consumer::ServiceEndpoint->fromHTML($id_url, $html); 324 return ("Fallback on link tag failed", undef) unless $endpoint; 325 return ($id_url, [$endpoint]); 326} 327 328sub beginWithoutDiscovery { 329 my ($self, $endpoint) = @_; 330 my $auth_req = $self->{consumer}->begin($endpoint); 331 $self->{session}->param($TOKEN_KEY, $auth_req->token); 332 return $auth_req; 333} 334 335=head2 complete 336 337=head3 Argument 338 339=over 340 341=item query 342 343Pass this method the query on the return_to url as a hash ref. 344Common ways to get this are C<CGI::Vars> and 345C<URI::QueryParam::query_form_hash> 346 347=back 348 349=head3 Returns 350 351An instance of one of the following objects. They all support the 352'status' method. 353 354=over 355 356=item L</Net::OpenID::JanRain::Consumer::SuccessResponse> 357 358=item L</Net::OpenID::JanRain::Consumer::FailureResponse> 359 360=item L</Net::OpenID::JanRain::Consumer::CancelResponse> 361 362=item L</Net::OpenID::JanRain::Consumer::SetupNeededResponse> 363 364=back 365 366=cut 367 368sub complete { 369 my ($self, $query) = @_; 370 371 my $token = $self->{session}->param($TOKEN_KEY); 372 373 my $response; 374 unless(defined($token)) { 375 $response = $failureResponseClass->new(undef, "Token not found in session"); 376 } 377 else { 378 $response = $self->{consumer}->complete($query, $token); 379 $self->{session}->clear([$TOKEN_KEY]); 380 } 381 if( ($response->status eq 'success' or $response->status eq 'cancel') 382 and defined($response->identity_url) ) 383 { 384 # Clean up the session - we're done. 385 $self->{session}->clear([$ENDPOINTS_KEY]); 386 } 387 return $response; 388} 389sub _normalizeUrl { 390 my $url = shift; 391 defined($url) or return undef; 392 $url = "http://$url" unless($url =~ m#^\w+://#); 393 return(URI->new($url)->canonical); 394} 395 396package DiffieHellmanConsumerSession; 397 398use Crypt::DH; 399 400use Net::OpenID::JanRain::Util( fromBase64 ); 401use Net::OpenID::JanRain::CryptUtil qw( DEFAULT_DH_MOD 402 DEFAULT_DH_GEN 403 numToBase64 404 base64ToNum 405 numToBytes 406 sha1 407 ); 408sub session_type { 409 return 'DH-SHA1'; 410} 411 412sub new { 413 my $caller = shift; 414 my $class = ref($caller) || $caller; 415 my $dh = shift; 416 417 my $default_dh = 0; 418 419 unless($dh) { 420 $dh = Crypt::DH->new(p => DEFAULT_DH_MOD, g=> DEFAULT_DH_GEN); 421 $default_dh = 1; 422 } 423 424 unless($dh->isa('Crypt::DH')) { 425 die "Attempt to instantiate DiffieHellmanConsumerSession with something not a Crypt::DH" 426 } 427 $dh->generate_keys; 428 429 my $self = { 430 dh => $dh, 431 default_dh => $default_dh, 432 }; 433 434 bless($self, $class); 435} 436 437sub dh { 438 my $self = shift; 439 return $self->{dh}; 440} 441 442sub request { 443 my $self = shift; 444 my $cpub = numToBase64($self->dh->pub_key); 445 446 my $args = {'openid.dh_consumer_public' => $cpub}; 447 448 unless($self->{default_dh}) { 449 $args->{'openid.dh_modulus'} = numToBase64($self->dh->p); 450 $args->{'openid.dh_gen'} = numToBase64($self->dh->g); 451 } 452 return $args; 453} 454 455sub extractSecret { 456 my ($self, $response) = @_; 457 458 my $spub = base64ToNum($response->{'dh_server_public'}); 459 my $dh_secret = $self->dh->compute_secret($spub); 460 my $enc_mac_key = fromBase64($response->{'enc_mac_key'}); 461 return ($enc_mac_key ^ sha1(numToBytes($dh_secret))); 462} 463 464package PlainTextConsumerSession; 465 466use Net::OpenID::JanRain::Util( fromBase64 ); 467 468 469sub session_type { 470 return undef; 471} 472 473sub new { 474 bless {}; 475} 476 477sub request { 478 return {}; 479} 480 481sub extractSecret { 482 my ($self, $response) = @_; 483 return fromBase64($response->{'mac_key'}); 484} 485 486package Net::OpenID::JanRain::Consumer::ServiceEndpoint; 487 488use Net::OpenID::JanRain::Consumer::LinkParser qw( parseOpenIDLinkRel ); 489 490sub new { 491 my $caller = shift; 492 my $class = ref($caller) || $caller; 493 494 my $self = { 495 type_uris => [], 496 }; 497 498 bless($self, $class); 499} 500 501sub fromHTML { 502 my ($caller, $uri, $html) = @_; 503 504 my ($delegate_url, $server_url) = parseOpenIDLinkRel($html); 505 unless (defined($server_url)) { 506 # warn "Could not find link tag"; 507 return undef; 508 } 509 my $service = $caller->new; 510 $service->{identity_url} = $uri; 511 $service->{delegate} = $delegate_url; 512 $service->{server_url} = $server_url; 513 $service->{type_uris} = [OPENID_1_0_TYPE]; 514 return $service; 515} 516 517sub usesExtension { 518 my ($self, $extension_uri) = @_; 519 foreach (@{$self->{type_uris}}) { 520 return 1 if $_ eq $extension_uri; 521 } 522 return 0; 523} 524 525sub server_id { 526 my $self = shift; 527 return $self->{delegate} || $self->{identity_url}; 528} 529 530sub identity_url { 531 my $self = shift; 532 return $self->{identity_url}; 533} 534 535sub server_url { 536 my $self = shift; 537 return $self->{server_url}; 538} 539 540package Net::OpenID::JanRain::GenericConsumer; 541 542use warnings; 543use strict; 544 545use Carp; 546use URI; 547use URI::QueryParam; 548use Net::OpenID::JanRain::Util qw( 549 appendArgs 550 toBase64 551 fromBase64 552 kvToHash 553 hashToKV 554 findAgent 555 ); 556 557use Net::OpenID::JanRain::CryptUtil qw( 558 randomString 559 hmacSha1 560 sha1 561 numToBase64 562 base64ToNum 563 numToBytes 564 bytesToNum 565 ); 566 567use Net::OpenID::JanRain::Consumer::LinkParser qw(parseLinkAttrs); 568 569require Net::OpenID::JanRain::Association; 570require Crypt::DH; 571 572# Parse a query, returning the openid parameters, removing 573# the 'openid.' prefix from the keys 574sub getOpenIDParameters { 575 my ($query) = @_; 576 my %params; 577 while(my ($k, $v) = each(%$query)) { 578 if($k =~ m/^openid\./) { 579 $params{$k} = $v; 580 } 581 } 582 return(%params); 583} # end getOpenIDParameters 584######################################################################## 585 586my $NONCE_LEN = 8; 587my $NONCE_CHRS = join("", 'a'..'z', 'A'..'Z', 0..9); 588# Maximum time for a transaction: 5 minutes 589my $TOKEN_LIFETIME = 60 * 5; 590 591sub new { 592 my $caller = shift; 593 my $store = shift; 594 my $fetcher = shift; 595 my $class = ref($caller) || $caller; 596 unless (defined($store)) { 597 die "Cannot instantiate OpenID consumer without a store"; 598 } 599 unless (defined($fetcher)) { 600 my $agentClass = findAgent(); 601 $fetcher = $agentClass->new; 602 } 603 my $self = { 604 store => $store, 605 fetcher => $fetcher 606 }; 607 bless($self, $class); 608} # end new 609######################################################################## 610my $authRequestClass = "Net::OpenID::JanRain::Consumer::AuthRequest"; 611sub begin { 612 my $self = shift; 613 my ($service_endpoint) = @_; 614 return undef unless $service_endpoint; 615 616 my $nonce = $self->_createNonce(); 617 my $token = $self->_genToken($service_endpoint->identity_url, 618 $service_endpoint->server_id, 619 $service_endpoint->server_url); 620 my $assoc = $self->_getAssociation($service_endpoint->server_url); 621 my $request = $authRequestClass->new($token, $assoc, $service_endpoint); 622 $request->addReturnToArg('nonce', $nonce); 623 return $request; 624} 625 626######################################################################## 627 628sub complete { 629 my $self = shift; 630 my ($query, $token) = @_; 631 my $mode = $query->{'openid.mode'}; 632 my ($identity_url, $server_id, $server_url) = $self->_splitToken($token); 633 634 if($mode eq 'cancel') { 635 return $cancelResponseClass->new($identity_url); 636 } 637 elsif($mode eq 'error') { 638 my $error = $query->{'openid.error'}; 639 return $failureResponseClass->new($identity_url, $error); 640 } 641 elsif($mode eq 'id_res') { 642 return $failureResponseClass->new($identity_url, 643 "No session state found") unless $identity_url; 644 645 my $response = $self->_doIdRes($query, $identity_url, $server_id, $server_url); 646 if ($response->status eq 'success') { 647 return $self->_checkNonce($response, $query->{nonce}) 648 } 649 else { 650 return $response; 651 } 652 } 653 else { 654 return $failureResponseClass->new($identity_url, "Invalid mode: $mode"); 655 } 656} # end complete 657 658sub _checkNonce { 659 my ($self, $response, $nonce) = @_; 660 661 my $rt_uri = URI->new($response->return_to); 662 my $query = $rt_uri->query_form_hash; 663 while( my ($k, $v) = each %$query) { 664 if ($k eq 'nonce') { 665 if ($v eq $nonce) { 666 if ($self->store->useNonce($nonce)) { 667 return $response; 668 } 669 else { 670 return $failureResponseClass->new($response->identity_url, 671 "Nonce not found in store"); 672 } 673 } 674 else { 675 return $failureResponseClass->new($response->identity_url, 676 "Nonce mismatch"); 677 } 678 } 679 } 680 return $failureResponseClass->new($response->identity_url, 681 "Nonce missing from return_to: ".$response->return_to); 682} 683 684sub _createNonce { 685 my $self = shift; 686 my $nonce = randomString($NONCE_LEN, $NONCE_CHRS); 687 $self->store->storeNonce($nonce); 688 return $nonce; 689} 690 691sub _doIdRes { 692 my $self = shift; 693 my ($query, $consumer_id, $server_id, $server_url) = @_; 694 695 my $user_setup_url = $query->{'openid.user_setup_url'}; 696 return $setupNeededResponseClass->new($consumer_id, $user_setup_url) if $user_setup_url; 697 698 my $return_to = $query->{'openid.return_to'}; 699 my $server_id2 = $query->{'openid.identity'}; 700 my $assoc_handle = $query->{'openid.assoc_handle'}; 701 702 unless($return_to and $server_id and $assoc_handle) { 703 my $missing_fields = ''; 704 $missing_fields .= 'return_to,' unless $return_to; 705 $missing_fields .= 'server_id,' unless $server_id; 706 $missing_fields .= 'assoc_handle' unless $assoc_handle; 707 708 return $failureResponseClass->new($consumer_id, "Missing required fields $missing_fields"); 709 } 710 unless($server_id eq $server_id2) { 711 return $failureResponseClass->new($consumer_id, 712 "Server ID mismatch: query($server_id2) token($server_id)"); 713 } 714 my $assoc = $self->store->getAssociation($server_url, $assoc_handle); 715 716 if(not $assoc) { # We don't know this association - we must do check_auth 717 if ($self->_checkAuth($consumer_id, $query, $server_url)) { 718 return $successResponseClass->fromQuery($consumer_id, $query); 719 } 720 else { 721 return $failureResponseClass->new($consumer_id, 722 "Check_authentication Failed"); 723 } 724 } 725 if($assoc->expiresIn == 0) { # expired assoc. Redo from start. 726 return $failureResponseClass->new($consumer_id, 727 "Association with $server_url expired."); 728 } 729 # Assoc is good - check the signature 730 my $sig = $query->{'openid.sig'}; 731 my $signed = $query->{'openid.signed'}; 732 if((not $sig) or (not $signed)) { 733 return $failureResponseClass->new($consumer_id, 734 "Signature missing from id_res parameters"); 735 } 736 737 my @signed_list = split(',', $signed); 738 my $v_sig = $assoc->signHash($query, \@signed_list, 'openid.'); 739 if ($v_sig ne $sig) { 740 return $failureResponseClass->new($consumer_id, 741 "Signature Mismatch!"); 742 } 743 return $successResponseClass->fromQuery($consumer_id, $query); 744} # end _doIdRes 745######################################################################## 746sub _checkAuth { 747 my $self = shift; 748 my ($consumer_id, $query, $server_url) = @_; 749 my $request = $self->_createCheckAuthRequest($query); 750 return undef unless $request; 751 my $response = $self->{fetcher}->post($server_url, $request); 752 return undef unless $response; 753 return $self->_processCheckAuthResponse($response, $server_url); 754} # end _checkAuth 755 756sub _createCheckAuthRequest { 757 my ($self, $query) = @_; 758 my $signed = $query->{'openid.signed'}; 759 unless ($signed) { 760 carp "Signed list empty; check_authentication aborted"; 761 return undef; 762 } 763 my @check_fields = split /,/, $signed; 764 push @check_fields, ('assoc_handle', 'sig', 'signed', 'invalidate_handle'); 765 my $check_args = {}; 766 for my $field (@check_fields) { 767 $check_args->{'openid.'.$field} = $query->{'openid.'.$field} 768 if defined($query->{'openid.'.$field}); 769 } 770 $check_args->{'openid.mode'} = 'check_authentication'; 771 return $check_args; 772} 773 774sub _processCheckAuthResponse { 775 my ($self, $response, $server_url) = @_; 776 777 my $hr = kvToHash($response->content); 778 779 my $is_valid = $hr->{'is_valid'}; 780 781 my $invalidate_handle = $response->{'invalidate_handle'}; 782 783 $self->store->removeAssociation($server_url, $invalidate_handle) 784 if defined($invalidate_handle); 785 786 return 1 if $is_valid eq 'true'; 787 788 warn "Server $server_url responded to check_auth with is_valid:$is_valid"; 789 return 0; 790} 791######################################################################## 792 793######################################################################## 794sub _genToken { 795 my $self = shift; 796 my ($consumer_id, $server_id, $server_url) = @_; 797 my $joined = join("\x00", time, $consumer_id, $server_id, $server_url); 798 my $sig = hmacSha1($self->store->getAuthKey, $joined); 799 return(toBase64($sig.$joined)); 800} # end _genToken 801######################################################################## 802sub _splitToken { 803 my $self = shift; 804 my ($token) = @_; 805 carp "trying to split undef" unless defined $token; 806 $token = fromBase64($token); 807 return() if(length($token) < 20); 808 my ($sig, $joined) = (substr($token, 0, 20), substr($token, 20)); 809 return() if(hmacSha1($self->store->getAuthKey, $joined) ne $sig); 810 my @s = split(/\x00/, $joined); 811 return() if(@s != 4); 812 my ($timestamp, $consumer_id, $server_id, $server_url) = @s; 813 return() if($timestamp == 0 or 814 (($timestamp + $TOKEN_LIFETIME) < time) 815 ); 816 return($consumer_id, $server_id, $server_url); 817} # end _splitToken 818 819sub _getAssociation { 820 my $self = shift; 821 my ($server_url, $replace) = @_; 822 $replace ||= 0; 823 $self->store->isDumb and return(); 824 my $assoc = $self->store->getAssociation($server_url); 825 unless ($assoc and $assoc->expiresIn > $TOKEN_LIFETIME) { 826 my ($assoc_session, $args) = $self->_createAssociateRequest($server_url); 827 my $response = $self->{fetcher}->post($server_url, $args); 828 return undef unless $response; 829 my $results = kvToHash($response->content); 830 $assoc = $self->_parseAssociation($results, $assoc_session, $server_url); 831 } 832 return $assoc; 833} # end _getAssociation 834 835sub _createAssociateRequest { 836 my ($self, $server_url) = @_; 837 my $sessionClass; 838 if ($server_url =~ /^https:/) { 839 $sessionClass = 'PlainTextConsumerSession'; 840 } 841 else { 842 $sessionClass = 'DiffieHellmanConsumerSession'; 843 } 844 my $assoc_session = $sessionClass->new; 845 846 my $args = { 847 'openid.mode' => 'associate', 848 'openid.assoc_type' => 'HMAC-SHA1', 849 }; 850 851 $args->{'openid.session_type'} = $assoc_session->session_type 852 if $assoc_session->session_type; 853 854 my $request = $assoc_session->request; 855 while (my ($k, $v) = each %$request) { 856 $args->{$k} = $v; 857 } 858 return $assoc_session, $args; 859} 860 861sub _parseAssociation { 862 my ($self, $results, $assoc_session, $server_url) = @_; 863 864 #XXX logging 865 my $assoc_type = $results->{assoc_type} or return undef; 866 my $assoc_handle = $results->{assoc_handle} or return undef; 867 my $expires_in_str = $results->{expires_in} or return undef; 868 869 return undef unless $assoc_type eq 'HMAC-SHA1'; 870 my $expires_in = int($expires_in_str) or return undef; 871 872 my $session_type = $results->{session_type} || 'plaintext'; 873 unless ($session_type eq $assoc_session->session_type) { 874 if($session_type eq 'plaintext' ) { 875 warn "Falling back to plaintext assoc session from ". 876 $assoc_session->session_type; 877 $assoc_session = PlainTextConsumerSession->new; 878 } 879 else { 880 warn "Session type mismatch. Expected ".$assoc_session->session_type. 881 "; got $session_type"; 882 return undef; 883 } 884 } 885 my $secret = $assoc_session->extractSecret($results) or return undef; 886 887 my $assoc = Net::OpenID::JanRain::Association->fromExpiresIn($expires_in, 888 $assoc_handle, $secret, $assoc_type); 889 $self->store->storeAssociation($server_url, $assoc); 890 891 return $assoc; 892} 893 894sub store { 895 my $self = shift; 896 return $self->{store}; 897} 898 899package Net::OpenID::JanRain::Consumer::AuthRequest; 900 901use warnings; 902use strict; 903use Net::OpenID::JanRain::Util qw( appendArgs ); 904 905=head1 Net::OpenID::JanRain::Consumer::AuthRequest 906 907An instance of this class is returned by the C<begin> method of the 908C<Net::OpenID::JanRain::Consumer> object when fetching the identity URL 909succeeded. 910 911=head2 Methods 912 913=head3 status 914 915returns 'in_progress' 916 917=cut 918 919sub new { 920 my $caller = shift; 921 my ($token, $assoc, $endpoint) = @_; 922 my $class = ref($caller) || $caller; 923 my $self = { 924 token => $token, 925 endpoint => $endpoint, 926 assoc => $assoc, 927 extra_args => {}, 928 return_to_args => {}, 929 }; 930 bless($self, $class); 931 return($self); 932} 933 934sub token { 935 my $self = shift; 936 return $self->{token}; 937} 938 939sub endpoint { 940 my $self = shift; 941 return $self->{endpoint}; 942} 943 944sub assoc { 945 my $self = shift; 946 return $self->{assoc}; 947} 948 949sub extra_args { 950 my $self = shift; 951 return $self->{extra_args}; 952} 953 954sub return_to_args { 955 my $self = shift; 956 return $self->{return_to_args}; 957} 958 959=head3 addExtensionArg 960 961 $auth_req->addExtensionArg($namespace, $key, $value); 962 963Add an extension argument to the openid request. 964 965=head4 Arguments 966 967=over 968 969=item namespace 970 971A namespace string, for example C<'sreg'>. 972 973=item key 974 975The name of the argument. 976 977=item value 978 979The contents of the argument. 980 981=back 982 983=cut 984 985sub addExtensionArg { 986 my ($self, $namespace, $key, $value) = @_; 987 988 my $arg_name = join '.', ('openid', $namespace, $key); 989 $self->{extra_args}->{$arg_name} = $value; 990} 991 992sub addReturnToArg { 993 my ($self, $key, $value) = @_; 994 995 $self->{return_to_args}->{$key} = $value; 996} 997 998=head2 redirectURL 999 1000 $url = $auth_req->redirectURL($trust_root, $return_to, $immediate); 1001 1002This method returns a URL on the user's OpenID server to redirect the 1003user agent to. 1004 1005=head3 Arguments 1006 1007=over 1008 1009=item trust_root 1010 1011Provide the trust root for your site. The return_to URL must descend 1012from this trust root. 1013 1014=item return_to 1015 1016This is the URL that the server will redirect the user back to after 1017authenticating. 1018 1019=item immediate 1020 1021This is an optional flag to use immediate mode, which indicates to the 1022server that if the authentication is not possible without user 1023interaction, the user agent should be redirected back immediately instead 1024of displaying a page to do the required login or approval. Use this flag 1025if you are performing this request behind the scenes, as in a hidden IFRAME. 1026 1027=back 1028 1029=cut 1030 1031sub redirectURL { 1032 my $self = shift; 1033 my $trust_root = shift; 1034 my $return_to = shift; 1035 my $immediate = shift; 1036 1037 my $mode; 1038 if($immediate) { 1039 $mode = 'checkid_immediate'; 1040 } 1041 else { 1042 $mode = 'checkid_setup'; 1043 } 1044 1045 $return_to = appendArgs($return_to, $self->return_to_args); 1046 1047 my $redir_args = { 1048 'openid.mode' => $mode, 1049 'openid.identity' => $self->endpoint->server_id, 1050 'openid.return_to' => $return_to, 1051 'openid.trust_root' => $trust_root, 1052 }; 1053 1054 if ($self->assoc) { 1055 $redir_args->{'openid.assoc_handle'} = $self->assoc->handle; 1056 } 1057 1058 while( my ($k, $v) = each %{$self->extra_args}) { 1059 $redir_args->{$k} = $v; 1060 } 1061 return appendArgs($self->endpoint->server_url, $redir_args); 1062} 1063 1064sub status { 1065 return 'in_progress'; 1066} 1067 1068package Net::OpenID::JanRain::Consumer::SuccessResponse; 1069 1070=head1 Net::OpenID::JanRain::Consumer::SuccessResponse 1071 1072This object is returned by the L</complete> method of 1073C<Net::OpenID::JanRain::Consumer> when the authentication 1074was successful. 1075 1076=head2 Methods 1077 1078=head3 extensionResponse 1079 1080Pass this method an extension prefix, and it will return a hash ref 1081with the parameters recieved for that extension. For example, if 1082the server sent the following response: 1083 1084 openid.mode=id_res 1085 openid.identity=http://bobdobbs.com/ 1086 openid.signed=[whatever] 1087 openid.sig=[whatever] 1088 openid.assoc_handle=[whatever] 1089 openid.return_to=[whatever] 1090 openid.sreg.fullname=Bob Dobbs 1091 openid.sreg.language=AQ 1092 1093Then once we had the success response we could do: 1094 1095 $response->extensionResponse('sreg'); 1096 --> {'fullname' => "Bob Dobbs", 'language' => 'AQ'} 1097 1098=head3 identity_url 1099 1100Returns the identity URL verified. 1101 1102=head3 return_to 1103 1104Returns the signed openid.return_to argument. 1105 1106=head3 status 1107 1108Returns 'success'. 1109 1110=cut 1111 1112sub new { 1113 my ($caller, $identity_url, $signed_args) = @_; 1114 my $class = ref($caller) || $caller; 1115 my $self = { identity_url => $identity_url, 1116 signed_args => $signed_args, 1117 }; 1118 bless($self,$class); 1119} 1120 1121sub fromQuery { 1122 my ($caller, $identity_url, $query) = @_; 1123 my @signed = split /,/,$query->{'openid.signed'}; 1124 my $signed_args = {}; 1125 foreach my $field (@signed) { 1126 $field = "openid.$field"; 1127 $signed_args->{$field} = $query->{$field}; 1128 } 1129 return $caller->new($identity_url, $signed_args); 1130} 1131 1132sub extensionResponse { 1133 my ($self, $prefix) = @_; 1134 my $response = {}; 1135 $prefix = "openid.$prefix."; 1136 while ( my ($k, $v) = each %{$self->{signed_args}}) { 1137 if($k =~ /^$prefix(.+)$/) { 1138 $response->{$1} = $v; 1139 } 1140 } 1141 return $response; 1142} 1143 1144sub identity_url { 1145 my $self = shift; 1146 return $self->{identity_url}; 1147} 1148 1149sub return_to { 1150 my $self = shift; 1151 return $self->{signed_args}->{'openid.return_to'}; 1152} 1153 1154sub status { 1155 return 'success'; 1156} 1157 1158package Net::OpenID::JanRain::Consumer::FailureResponse; 1159 1160=head1 Net::OpenID::JanRain::Consumer::FailureResponse 1161 1162An instance of this class may be returned by the L</begin> or L</complete> 1163methods of the C<Net::OpenID::JanRain::Consumer>. It indicates protocol 1164failure. 1165 1166=head2 Methods 1167 1168=head3 status 1169 1170returns 'failure' 1171 1172=head3 identity_url 1173 1174returns the identity url in question. 1175 1176=head3 message 1177 1178returns a message describing the failure. 1179 1180 1181=cut 1182 1183sub new { 1184 my ($caller,$identity_url, $message) = @_; 1185 1186 my $self = { identity_url => $identity_url, 1187 message => $message, 1188 }; 1189 my $class = ref($caller) || $caller; 1190 bless($self,$class); 1191} 1192 1193sub status { 1194 return 'failure'; 1195} 1196 1197sub identity_url { 1198 my $self = shift; 1199 return $self->{identity_url}; 1200} 1201 1202sub message { 1203 my $self = shift; 1204 return $self->{message}; 1205} 1206 1207package Net::OpenID::JanRain::Consumer::CancelResponse; 1208 1209=head1 Net::OpenID::JanRain::Consumer::CancelResponse 1210 1211This object is returned by the L</complete> method of 1212C<Net::OpenID::JanRain::Consumer> when a cancel response was 1213recieved from the server, indicating that the user did not 1214complete the authentication process. 1215 1216=head2 Methods 1217 1218=head3 status 1219 1220returns 'cancel' 1221 1222=head3 identity_url 1223 1224returns the identity url of the request, if available. 1225 1226=cut 1227 1228sub new { 1229 my $caller = shift; 1230 my $identity_url = shift; 1231 my $self = {identity_url => $identity_url}; 1232 1233 my $class = ref($caller) || $caller; 1234 bless($self,$class); 1235} 1236 1237sub status { 1238 return 'cancel'; 1239} 1240 1241sub identity_url { 1242 my $self = shift; 1243 return $self->{identity_url}; 1244} 1245 1246package Net::OpenID::JanRain::Consumer::SetupNeededResponse; 1247 1248=head1 Net::OpenID::JanRain::Consumer::SetupNeededResponse 1249 1250An instance of this class is returned by the L</complete> method of 1251C<Net::OpenID::JanRain::Consumer> when an immediate mode request 1252was not successful. You must instead use non-immediate mode. A URL 1253to send the user to is provided. 1254 1255=head2 Methods 1256 1257=head3 status 1258 1259returns 'setup_needed' 1260 1261=head3 setup_url 1262 1263returns the setup url, where you may redirect the user to complete 1264authentication. 1265 1266=head3 identity_url 1267 1268returns the identity url in question. 1269 1270=cut 1271 1272sub new { 1273 my $caller = shift; 1274 my $identity_url = shift; 1275 my $setup_url = shift; 1276 1277 my $self = {identity_url => $identity_url, 1278 setup_url => $setup_url}; 1279 1280 my $class = ref($caller) || $caller; 1281 bless($self,$class); 1282} 1283 1284sub status { 1285 return 'setup_needed'; 1286} 1287 1288sub setup_url { 1289 my $self = shift; 1290 return $self->{setup_url}; 1291} 1292 1293sub identity_url { 1294 my $self = shift; 1295 return $self->{identity_url}; 1296} 1297 12981; 1299