1package Net::OAuth::Simple;
2
3use warnings;
4use strict;
5our $VERSION = "1.7";
6
7use URI;
8use LWP;
9use CGI;
10use HTTP::Request::Common ();
11use Carp;
12use Net::OAuth;
13use Scalar::Util qw(blessed);
14use Digest::SHA;
15use File::Basename;
16require Net::OAuth::Request;
17require Net::OAuth::RequestTokenRequest;
18require Net::OAuth::AccessTokenRequest;
19require Net::OAuth::ProtectedResourceRequest;
20require Net::OAuth::XauthAccessTokenRequest;
21require Net::OAuth::UserAuthRequest;
22
23BEGIN {
24    eval {  require Math::Random::MT };
25    unless ($@) {
26        Math::Random::MT->import(qw(srand rand));
27    }
28}
29
30our @required_constructor_params = qw(consumer_key consumer_secret);
31our @access_token_params         = qw(access_token access_token_secret);
32our @general_token_params        = qw(general_token general_token_secret);
33
34=head1 NAME
35
36Net::OAuth::Simple - a simple wrapper round the OAuth protocol
37
38=head1 SYNOPSIS
39
40First create a sub class of C<Net::OAuth::Simple> that will do you requests
41for you.
42
43    package Net::AppThatUsesOAuth;
44
45    use strict;
46    use base qw(Net::OAuth::Simple);
47
48
49    sub new {
50        my $class  = shift;
51        my %tokens = @_;
52        return $class->SUPER::new( tokens => \%tokens,
53                                   protocol_version => '1.0a',
54                                   urls   => {
55                                        authorization_url => ...,
56                                        request_token_url => ...,
57                                        access_token_url  => ...,
58                                   });
59    }
60
61    sub view_restricted_resource {
62        my $self = shift;
63        my $url  = shift;
64        return $self->make_restricted_request($url, 'GET');
65    }
66
67    sub update_restricted_resource {
68        my $self         = shift;
69        my $url          = shift;
70        my %extra_params = @_;
71        return $self->make_restricted_request($url, 'POST', %extra_params);
72    }
73    1;
74
75
76Then in your main app you need to do
77
78    # Get the tokens from the command line, a config file or wherever
79    my %tokens  = get_tokens();
80    my $app     = Net::AppThatUsesOAuth->new(%tokens);
81
82    # Check to see we have a consumer key and secret
83    unless ($app->consumer_key && $app->consumer_secret) {
84        die "You must go get a consumer key and secret from App\n";
85    }
86
87    # If the app is authorized (i.e has an access token and secret)
88    # Then look at a restricted resourse
89    if ($app->authorized) {
90        my $response = $app->view_restricted_resource;
91        print $response->content."\n";
92        exit;
93    }
94
95
96    # Otherwise the user needs to go get an access token and secret
97    print "Go to ".$app->get_authorization_url."\n";
98    print "Then hit return after\n";
99    <STDIN>;
100
101    my ($access_token, $access_token_secret) = $app->request_access_token;
102
103    # Now save those values
104
105
106Note the flow will be somewhat different for web apps since the request token
107and secret will need to be saved whilst the user visits the authorization url.
108
109For examples go look at the C<Net::FireEagle> module and the C<fireeagle> command
110line script that ships with it. Also in the same distribution in the C<examples/>
111directory is a sample web app.
112
113=head1 METHODS
114
115=cut
116
117=head2 new [params]
118
119Create a new OAuth enabled app - takes a hash of params.
120
121One of the keys of the hash must be C<tokens>, the value of which
122must be a hash ref with the keys:
123
124=over 4
125
126=item consumer_key
127
128=item consumer_secret
129
130=back
131
132Then, when you have your per-use access token and secret you
133can supply
134
135=over 4
136
137=item access_token
138
139=item access_secret
140
141=back
142
143Another key of the hash must be C<urls>, the value of which must
144be a hash ref with the keys
145
146=over 4
147
148=item authorization_url
149
150=item request_token_url
151
152=item access_token_url
153
154=back
155
156If you pass in a key C<protocol_version> with a value equal to B<1.0a> then
157the newest version of the OAuth protocol will be used. A value equal to B<1.0> will
158mean the old version will be used. Defaults to B<1.0a>
159
160You can pass in your own User Agent by using the key C<browser>.
161
162If you pass in C<return_undef_on_error> then instead of C<die>-ing on error
163methods will return undef instead and the error can be retrieved using the
164C<last_error()> method. See the section on B<ERROR HANDLING>.
165
166=cut
167
168sub new {
169    my $class  = shift;
170    my %params = @_;
171    $params{protocol_version} ||= '1.0a';
172    my $client = bless \%params, $class;
173
174    # Set up LibWWWPerl for HTTP requests
175    $client->{browser} ||= LWP::UserAgent->new;
176
177    # Verify arguments
178    $client->_check;
179
180    # Client Object
181    return $client;
182}
183
184# Validate required constructor params
185sub _check {
186    my $self = shift;
187
188    foreach my $param ( @required_constructor_params ) {
189        unless ( defined $self->{tokens}->{$param} ) {
190            return $self->_error("Missing required parameter '$param'");
191        }
192    }
193
194    return $self->_error("browser must be a LWP::UserAgent")
195        unless blessed $self->{browser} && $self->{browser}->isa('LWP::UserAgent');
196}
197
198=head2 oauth_1_0a
199
200Whether or not we're using 1.0a version of OAuth (necessary for,
201amongst others, FireEagle)
202
203=cut
204sub oauth_1_0a {
205    my $self = shift;
206    return $self->{protocol_version } eq '1.0a';
207}
208
209=head2 authorized
210
211Whether the client has the necessary credentials to be authorized.
212
213Note that the credentials may be wrong and so the request may still fail.
214
215=cut
216
217sub authorized {
218    my $self = shift;
219    foreach my $param ( @access_token_params ) {
220        return 0 unless defined $self->{tokens}->{$param} && length $self->{tokens}->{$param};
221    }
222    return 1;
223}
224
225=head2 signature_method [method]
226
227The signature method to use.
228
229Defaults to HMAC-SHA1
230
231=cut
232sub signature_method {
233    my $self = shift;
234    $self->{signature_method} = shift if @_;
235    return $self->{signature_method} || 'HMAC-SHA1';
236}
237
238=head2 tokens
239
240Get all the tokens.
241
242=cut
243sub tokens {
244    my $self = shift;
245    if (@_) {
246        my %tokens = @_;
247        $self->{tokens} = \%tokens;
248    }
249    return %{$self->{tokens}||{}};
250}
251
252=head2 consumer_key [consumer key]
253
254Returns the current consumer key.
255
256Can optionally set the consumer key.
257
258=cut
259
260sub consumer_key {
261    my $self = shift;
262    $self->_token('consumer_key', @_);
263}
264
265=head2 consumer_secret [consumer secret]
266
267Returns the current consumer secret.
268
269Can optionally set the consumer secret.
270
271=cut
272
273sub consumer_secret {
274    my $self = shift;
275    $self->_token('consumer_secret', @_);
276}
277
278
279=head2 access_token [access_token]
280
281Returns the current access token.
282
283Can optionally set a new token.
284
285=cut
286
287sub access_token {
288    my $self = shift;
289    $self->_token('access_token', @_);
290}
291
292=head2 access_token_secret [access_token_secret]
293
294Returns the current access token secret.
295
296Can optionally set a new secret.
297
298=cut
299
300sub access_token_secret {
301    my $self = shift;
302    return $self->_token('access_token_secret', @_);
303}
304
305=head2 general_token [token]
306
307Get or set the general token.
308
309See documentation in C<new()>
310
311=cut
312
313sub general_token {
314     my $self = shift;
315     $self->_token('general_token', @_);
316}
317
318=head2 general_token_secret [secret]
319
320Get or set the general token secret.
321
322See documentation in C<new()>
323
324=cut
325
326sub general_token_secret {
327    my $self = shift;
328    $self->_token('general_token_secret', @_);
329}
330
331=head2 authorized_general_token
332
333Is the app currently authorized for general token requests.
334
335See documentation in C<new()>
336
337=cut
338
339sub authorized_general_token {
340     my $self = shift;
341     foreach my $param ( @general_token_params ) {
342        return 0 unless defined $self->$param();
343     }
344     return 1;
345}
346
347
348=head2 request_token [request_token]
349
350Returns the current request token.
351
352Can optionally set a new token.
353
354=cut
355
356sub request_token {
357    my $self = shift;
358    $self->_token('request_token', @_);
359}
360
361
362=head2 request_token_secret [request_token_secret]
363
364Returns the current request token secret.
365
366Can optionally set a new secret.
367
368=cut
369
370sub request_token_secret {
371    my $self = shift;
372    return $self->_token('request_token_secret', @_);
373}
374
375=head2 verifier [verifier]
376
377Returns the current oauth_verifier.
378
379Can optionally set a new verifier.
380
381=cut
382
383sub verifier {
384    my $self = shift;
385    return $self->_param('verifier', @_);
386}
387
388=head2 callback [callback]
389
390Returns the oauth callback.
391
392Can optionally set the oauth callback.
393
394=cut
395
396sub callback {
397    my $self = shift;
398    $self->_param('callback', @_);
399}
400
401=head2 callback_confirmed [callback_confirmed]
402
403Returns the oauth callback confirmed.
404
405Can optionally set the oauth callback confirmed.
406
407=cut
408
409sub callback_confirmed {
410    my $self = shift;
411    $self->_param('callback_confirmed', @_);
412}
413
414
415sub _token {
416    my $self = shift;
417    $self->_store('tokens', @_);
418}
419
420sub _param {
421    my $self = shift;
422    $self->_store('params', @_);
423}
424
425sub _store {
426    my $self = shift;
427    my $ns   = shift;
428    my $key  = shift;
429    $self->{$ns}->{$key} = shift if @_;
430    return $self->{$ns}->{$key};
431}
432
433=head2 authorization_url
434
435Get the url the user needs to visit to authorize as a URI object.
436
437Note: this is the base url - not the full url with the necessary OAuth params.
438
439=cut
440sub authorization_url {
441    my $self = shift;
442    return $self->_url('authorization_url', @_);
443}
444
445
446=head2 request_token_url
447
448Get the url to obtain a request token as a URI object.
449
450=cut
451sub request_token_url {
452    my $self = shift;
453    return $self->_url('request_token_url', @_);
454}
455
456=head2 access_token_url
457
458Get the url to obtain an access token as a URI object.
459
460=cut
461sub access_token_url {
462    my $self = shift;
463    return $self->_url('access_token_url', @_);
464}
465
466sub _url {
467    my $self = shift;
468    my $key  = shift;
469    $self->{urls}->{$key} = shift if @_;
470    my $url  = $self->{urls}->{$key} || return;;
471    return URI->new($url);
472}
473
474# generate a random number
475sub _nonce {
476    return int( rand( 2**32 ) );
477}
478
479=head2 request_access_token [param[s]]
480
481Request the access token and access token secret for this user.
482
483The user must have authorized this app at the url given by
484C<get_authorization_url> first.
485
486Returns the access token and access token secret but also sets
487them internally so that after calling this method you can
488immediately call a restricted method.
489
490If you pass in a hash of params then they will added as parameters to the URL.
491
492=cut
493
494sub request_access_token {
495    my $self   = shift;
496    my %params = @_;
497    my $url    = $self->access_token_url;
498
499    $params{token}        = $self->request_token        unless defined $params{token};
500    $params{token_secret} = $self->request_token_secret unless defined $params{token_secret};
501
502    if ($self->oauth_1_0a) {
503        $params{verifier} = $self->verifier                             unless defined $params{verifier};
504        return $self->_error("You must pass a verified parameter when using OAuth v1.0a") unless defined $params{verifier};
505
506    }
507
508
509    my $access_token_response = $self->_make_request(
510        'Net::OAuth::AccessTokenRequest',
511        $url, 'POST',
512        %params,
513    );
514
515    return $self->_decode_tokens($url, $access_token_response);
516}
517
518sub _decode_tokens {
519    my $self                  = shift;
520    my $url                   = shift;
521    my $access_token_response = shift;
522
523    # Cast response into CGI query for EZ parameter decoding
524    my $access_token_response_query =
525      new CGI( $access_token_response->content );
526
527    # Split out token and secret parameters from the access token response
528    $self->access_token($access_token_response_query->param('oauth_token'));
529    $self->access_token_secret($access_token_response_query->param('oauth_token_secret'));
530
531    delete $self->{tokens}->{$_} for qw(request_token request_token_secret verifier);
532
533    return $self->_error("$url did not reply with an access token")
534      unless ( $self->access_token && $self->access_token_secret );
535
536    return ( $self->access_token, $self->access_token_secret );
537
538}
539
540=head2 xauth_request_access_token [param[s]]
541
542The same as C<request_access_token> but for xAuth.
543
544For more information on xAuth see
545
546    http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-oauth-access_token-for-xAuth
547
548You must pass in the parameters
549
550    x_auth_username
551    x_auth_password
552    x_auth_mode
553
554You must have HTTPS enabled for LWP::UserAgent.
555
556See C<examples/twitter_xauth> for a sample implementation.
557
558=cut
559sub xauth_request_access_token {
560    my $self = shift;
561    my %params = @_;
562    my $url = $self->access_token_url;
563    $url =~ s !^http:!https:!; # force https
564
565    my %xauth_params = map { $_ => $params{$_} }
566        grep {/^x_auth_/}
567        @{Net::OAuth::XauthAccessTokenRequest->required_message_params};
568
569    my $access_token_response = $self->_make_request(
570        'Net::OAuth::XauthAccessTokenRequest',
571        $url, 'POST',
572        %xauth_params,
573    );
574
575    return $self->_decode_tokens($url, $access_token_response);
576}
577
578=head2 request_request_token [param[s]]
579
580Request the request token and request token secret for this user.
581
582This is called automatically by C<get_authorization_url> if necessary.
583
584If you pass in a hash of params then they will added as parameters to the URL.
585
586=cut
587
588
589sub request_request_token {
590    my $self   = shift;
591    my %params = @_;
592    my $url    = $self->request_token_url;
593
594    if ($self->oauth_1_0a) {
595        $params{callback} = $self->callback                             unless defined $params{callback};
596        return $self->_error("You must pass a callback parameter when using OAuth v1.0a") unless defined $params{callback};
597    }
598
599    my $request_token_response = $self->_make_request(
600        'Net::OAuth::RequestTokenRequest',
601        $url, 'GET',
602        %params);
603
604    return $self->_error("GET for $url failed: ".$request_token_response->status_line)
605      unless ( $request_token_response->is_success );
606
607    # Cast response into CGI query for EZ parameter decoding
608    my $request_token_response_query =
609      new CGI( $request_token_response->content );
610
611    # Split out token and secret parameters from the request token response
612    $self->request_token($request_token_response_query->param('oauth_token'));
613    $self->request_token_secret($request_token_response_query->param('oauth_token_secret'));
614    $self->callback_confirmed($request_token_response_query->param('oauth_callback_confirmed'));
615
616    # Hack to deal with bug in older versions of oauth-php (See https://code.google.com/p/oauth-php/issues/detail?id=60)
617    $self->callback_confirmed($request_token_response_query->param('oauth_callback_accepted'))
618      unless $self->callback_confirmed;
619
620    return $self->_error("Response does not confirm to OAuth1.0a. oauth_callback_confirmed not received")
621     if $self->oauth_1_0a && !$self->callback_confirmed;
622
623}
624
625=head2 get_authorization_url [param[s]]
626
627Get the URL to authorize a user as a URI object.
628
629If you pass in a hash of params then they will added as parameters to the URL.
630
631=cut
632
633sub get_authorization_url {
634    my $self   = shift;
635    my %params = @_;
636    my $url  = $self->authorization_url;
637    if (!defined $self->request_token) {
638        $self->request_request_token(%params);
639    }
640    #$params{oauth_token}     = $self->request_token;
641    $url->query_form(%params);
642    my $req = $self->_build_request('Net::OAuth::UserAuthRequest', $url, "GET");
643    return $req->normalized_request_url;
644}
645
646=head2 make_restricted_request <url> <HTTP method> [extra[s]]
647
648Make a request to C<url> using the given HTTP method.
649
650Any extra parameters can be passed in as a hash.
651
652=cut
653sub make_restricted_request {
654    my $self     = shift;
655
656    return $self->_error("This restricted request is not authorized") unless $self->authorized;
657
658    return $self->_restricted_request( $self->access_token, $self->access_token_secret, @_ );
659}
660
661=head2 make_general_request <url> <HTTP method> [extra[s]]
662
663Make a request to C<url> using the given HTTP method using
664the general purpose tokens.
665
666Any extra parameters can be passed in as a hash.
667
668=cut
669sub make_general_request {
670    my $self  = shift;
671
672    return $self->_error("This general request is not authorized") unless $self->authorized_general_token;
673
674    return $self->_restricted_request( $self->general_token, $self->general_token_secret, @_ );
675}
676
677sub _restricted_request {
678    my $self     = shift;
679    my $token    = shift;
680    my $secret   = shift;
681    my $url      = shift;
682    my $method   = shift;
683    my %extras   = @_;
684    my $response = $self->_make_request(
685        'Net::OAuth::ProtectedResourceRequest',
686        $url, $method,
687        token            => $token,
688        token_secret     => $secret,
689        extra_params     => \%extras
690    );
691    return $response;
692}
693
694sub _make_request {
695    my $self    = shift;
696    my $class   = shift;
697    my $url     = shift;
698    my $method  = uc(shift);
699    my @extra   = @_;
700
701    my $request  = $self->_build_request($class, $url, $method, @extra);
702    my $response = $self->{browser}->request($request);
703    return $self->_error("$method on ".$request->normalized_request_url." failed: ".$response->status_line." - ".$response->content)
704      unless ( $response->is_success );
705
706    return $response;
707}
708
709use Data::Dumper;
710sub _build_request {
711    my $self    = shift;
712    my $class   = shift;
713    my $url     = shift;
714    my $method  = uc(shift);
715    my @extra   = @_;
716
717    my $uri   = URI->new($url);
718    my %query = $uri->query_form;
719    $uri->query_form({});
720
721
722    my $content;
723    my $filename;
724    if ('PUT' eq $method) {
725      # Get the content (goes in the body), and hash the content for inclusion in the message
726      my %params = @extra;
727      $filename = delete $params{extra_params}->{filename};
728
729      return $self->_error('Missing required parameter $filename') unless $filename;
730
731      # Slurp the file from above
732      my $content = "";
733      $self->_read_file( $filename, sub { $content .= shift } ) ;
734      ($filename) = fileparse($filename);
735
736
737      # Net::OAuth doesn't seem to handle body hash, so do it ourselves
738
739      # Per http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html#parameter
740      # If the OAuth signature method is HMAC-SHA1 or RSA-SHA1, SHA1 MUST be used as the body hash algorithm
741      # No discussion of other signature methods, but the draft spec for OAuth2 predictably says that SHA256
742      # should be used for HMAC-SHA256
743
744      if ($self->signature_method eq 'HMAC-SHA1' || $self->signature_method eq 'RSA-SHA1') {
745        $params{body_hash} = Digest::SHA::sha1_hex($content);
746      } elsif ($self->signature_method eq 'HMAC-SHA256') {
747        $params{body_hash} = Digest::SHA::sha256_hex($content);
748      } else {
749        return $self->_error("Unknown signature method: ".$self->signature_method);
750      }
751
752      @extra = %params;
753    }
754
755
756
757    my $request = $class->new(
758        consumer_key     => $self->consumer_key,
759        consumer_secret  => $self->consumer_secret,
760        request_url      => $uri,
761        request_method   => $method,
762        signature_method => $self->signature_method,
763        protocol_version => $self->oauth_1_0a ? Net::OAuth::PROTOCOL_VERSION_1_0A : Net::OAuth::PROTOCOL_VERSION_1_0,
764        timestamp        => time,
765        nonce            => $self->_nonce,
766        extra_params     => \%query,
767        @extra,
768    );
769    $request->add_optional_message_params('body_hash') if 'PUT' eq $method;
770    $request->sign;
771    return $self->_error("Couldn't verify request! Check OAuth parameters.")
772      unless $request->verify;
773
774    my $req_url = ('GET' eq $method || 'DELETE' eq $method) ? $request->to_url() : $url;
775
776    my $req = HTTP::Request->new( $method => $req_url);
777
778    if ('PUT' eq $method) {
779      $req->header('Authorization' => $request->to_authorization_header(""));
780      $req->header('Content-disposition' => qq!attachment; filename="$filename"!);
781      $req->content($content);
782    }
783
784    if ('POST' eq $method) {
785      # "@extra" params are the ones that don't start with oath_ in the hash
786      # User passed them, they must want us to actually send them, huh?
787      $request->add_optional_message_params($_) for grep { ! /^oauth_/ } keys %{$request->to_hash};
788      $req->content_type('application/x-www-form-urlencoded');
789      $req->content($request->to_post_body);
790    }
791
792    return $req;
793}
794
795
796sub _error {
797    my $self = shift;
798    my $mess = shift;
799    if ($self->{return_undef_on_error}) {
800        $self->{_last_error} = $mess;
801    } else {
802        croak $mess;
803    }
804    return undef;
805}
806
807=head2 last_error
808
809Get the last error message.
810
811Only works if C<return_undef_on_error> was passed in to the constructor.
812
813See the section on B<ERROR HANDLING>.
814
815=cut
816sub last_error {
817    my $self = shift;
818    return $self->{_last_error};
819}
820
821=head2 load_tokens <file>
822
823A convenience method for loading tokens from a config file.
824
825Returns a hash with the token names suitable for passing to
826C<new()>.
827
828Returns an empty hash if the file doesn't exist.
829
830=cut
831sub load_tokens {
832    my $class  = shift;
833    my $file   = shift;
834    my %tokens = ();
835    return %tokens unless -f $file;
836
837    $class->_read_file($file, sub {
838        $_ = shift;
839        chomp;
840        next if /^#/;
841        next if /^\s*$/;
842        next unless /=/;
843        s/(^\s*|\s*$)//g;
844        my ($key, $val) = split /\s*=\s*/, $_, 2;
845        $tokens{$key} = $val;
846    });
847    return %tokens;
848}
849
850=head2 save_tokens <file> [token[s]]
851
852A convenience method to save a hash of tokens out to the given file.
853
854=cut
855sub save_tokens {
856    my $class  = shift;
857    my $file   = shift;
858    my %tokens = @_;
859
860    my $max    = 0;
861    foreach my $key (keys %tokens) {
862        $max   = length($key) if length($key)>$max;
863    }
864
865    open(my $fh, ">$file") || die "Couldn't open $file for writing: $!\n";
866    foreach my $key (sort keys %tokens) {
867        my $pad = " "x($max-length($key));
868        print $fh "$key ${pad}= ".$tokens{$key}."\n";
869    }
870    close($fh);
871}
872
873sub _read_file {
874    my $self = shift;
875    my $file = shift;
876    my $sub  = shift;
877
878    open(my $fh, $file) || die "Couldn't open $file: $!\n";
879    while (<$fh>) {
880        $sub->($_) if $sub;
881    }
882    close($fh);
883}
884
885=head1 ERROR HANDLING
886
887Originally this module would die upon encountering an error (inheriting behaviour
888from the original Yahoo! code).
889
890This is still the default behaviour however if you now pass
891
892    return_undef_on_error => 1
893
894into the constructor then all methods will return undef on error instead.
895
896The error message is accessible via the C<last_error()> method.
897
898=head1 GOOGLE'S SCOPE PARAMETER
899
900Google's OAuth API requires the non-standard C<scope> parameter to be set
901in C<request_token_url>, and you also explicitly need to pass an C<oauth_callback>
902to C<get_authorization_url()> method, so that you can direct the user to your site
903if you're authenticating users in Web Application mode. Otherwise Google will let
904user grant acesss as a desktop app mode and doesn't redirect users back.
905
906Here's an example class that uses Google's Portable Contacts API via OAuth:
907
908    package Net::AppUsingGoogleOAuth;
909    use strict;
910    use base qw(Net::OAuth::Simple);
911
912    sub new {
913        my $class  = shift;
914        my %tokens = @_;
915        return $class->SUPER::new(
916            tokens => \%tokens,
917            urls   => {
918                request_token_url => "https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www-opensocial.googleusercontent.com/api/people",
919                authorization_url => "https://www.google.com/accounts/OAuthAuthorizeToken",
920                access_token_url  => "https://www.google.com/accounts/OAuthGetAccessToken",
921            },
922        );
923    }
924
925    package main;
926    my $oauth = Net::AppUsingGoogleOAuth->new(%tokens);
927
928    # Web application
929    $app->redirect( $oauth->get_authorization_url( callback => "http://you.example.com/oauth/callback") );
930
931    # Desktop application
932    print "Open the URL and come back once you're authenticated!\n",
933        $oauth->get_authorization_url;
934
935See L<http://code.google.com/apis/accounts/docs/OAuth.html> and other
936services API documentation for the possible list of I<scope> parameter value.
937
938=head1 RANDOMNESS
939
940If C<Math::Random::MT> is installed then any nonces generated will use a
941Mersenne Twiser instead of Perl's built in randomness function.
942
943=head1 EXAMPLES
944
945There are example Twitter and Twitter xAuth 'desktop' apps and a FireEagle OAuth 1.0a web app
946in the examples directory of the distribution.
947
948=head1 BUGS
949
950Non known
951
952=head1 DEVELOPERS
953
954The latest code for this module can be found at
955
956    https://svn.unixbeard.net/simon/Net-OAuth-Simple
957
958=head1 AUTHOR
959
960Simon Wistow, C<<simon@thegestalt.org>>
961
962=head1 BUGS
963
964Please report any bugs or feature requests to C<bug-net-oauth-simple at rt.cpan.org>, or through
965the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-OAuth-Simple>.  I will be notified, and then you'll
966automatically be notified of progress on your bug as I make changes.
967
968=head1 SUPPORT
969
970You can find documentation for this module with the perldoc command.
971
972    perldoc Net::OAuth::Simple
973
974
975You can also look for information at:
976
977=over 4
978
979=item * RT: CPAN's request tracker
980
981L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-OAuth-Simple>
982
983=item * AnnoCPAN: Annotated CPAN documentation
984
985L<http://annocpan.org/dist/Net-OAuth-Simple>
986
987=item * CPAN Ratings
988
989L<http://cpanratings.perl.org/d/Net-OAuth-Simple>
990
991=item * Search CPAN
992
993L<http://search.cpan.org/dist/Net-OAuth-Simple/>
994
995=back
996
997=head1 COPYRIGHT & LICENSE
998
999Copyright 2009 Simon Wistow, all rights reserved.
1000
1001This program is free software; you can redistribute it and/or modify it
1002under the same terms as Perl itself.
1003
1004
1005=cut
1006
10071; # End of Net::OAuth::Simple
1008