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