File Coverage

blib/lib/Net/OAuth2/Moosey/Client.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Net::OAuth2::Moosey::Client;
2 1     1   21473 use Moose;
  0            
  0            
3              
4             =head1 NAME
5              
6             Net::OAuth2::Moosey::Client - OAuth 2.0 client for perl
7              
8             =head1 VERSION
9              
10             0.02
11              
12             =cut
13              
14             our $VERSION = '0.02';
15              
16             =head1 DESCRIPTION
17              
18             This is a perl implementation of the OAuth 2.0 protocol.
19              
20             It is based on (forked from), and very similar in functionality to Keith Grennan's L<Net::OAuth2> module.
21              
22             The major differences to the original L<Net::OAuth2> module are:
23              
24             =over 2
25              
26             =item * Converted to use Moose
27              
28             =item * Named parameters for all methods
29              
30             =item * More documentation
31              
32             =item * No demo code for a web application
33              
34             =back
35              
36             =head1 SYNOPSIS
37              
38             use Net::OAuth2::Moosey::Client;
39             my %client_params = (
40             site_url_base => 'https://accounts.google.com/o/oauth2/auth',
41             access_token_url_base => 'https://accounts.google.com/o/oauth2/token',
42             authorize_url_base => 'https://accounts.google.com/o/oauth2/auth',
43             scope => 'https://www.google.com/fusiontables/api/query',
44             client_id => '123456789.apps.googleusercontent.com',
45             client_secret => 'atecSNTE23sthbjcasrCuw4i',
46             );
47              
48             my $client = Net::OAuth2::Moosey::Client->new( %client_params );
49              
50             # Get a fresh access token
51             $client->get_fresh_access_token();
52              
53             # Send a request
54             my @post_args = ( 'https://www.google.com/fusiontables/api/query',
55             HTTP::Headers->new( Content_Type => 'application/x-www-form-urlencoded' ),
56             sprintf( 'sql=%s', url_encode( 'SHOW TABLES' ) ) );
57             my $response = $self->auth_client->post( @post_args );
58              
59              
60             =cut
61              
62             use Carp;
63             use LWP::UserAgent;
64             use URI;
65             use JSON;
66             use HTTP::Request;
67             use HTTP::Request::Common;
68             use Net::OAuth2::Moosey::AccessToken;
69             use MooseX::Types::URI qw(Uri FileUri DataUri);
70             use MooseX::Log::Log4perl;
71             use YAML;
72              
73             =head1 METHODS
74              
75             =head2 new
76              
77             =head3 ATTRIBUTES
78              
79             =over 2
80              
81             =item * client_id <Str>
82              
83             ID for your application as given to you by your service provider.
84              
85             =item * client_secret <Str>
86              
87             Secret for your application as given to you by your service provider.
88              
89             =item * scope <Uri>
90              
91             Scope for which your are applying for access to.
92              
93             e.g. https://www.google.com/fusiontables/api/query
94              
95             =item * site_url_base <Uri>
96              
97             Base url for OAuth.
98              
99             e.g. https://accounts.google.com/o/oauth2/auth
100              
101             =item * access_token_url_base <Uri>
102              
103             Access token url.
104              
105             e.g. https://accounts.google.com/o/oauth2/token
106              
107             =item * authorize_url_base <Uri>
108              
109             Authorize url.
110              
111             e.g. https://accounts.google.com/o/oauth2/auth
112              
113             =item * access_token_path <Str>
114              
115             =item * authorize_path <Str>
116              
117             The ..._path parameters are an alternative to their ..._url_base counterparts.
118             If used, the authorize_url will be built from the site_url_base and the _path.
119              
120             =item * refresh_token <Str>
121              
122             If known, the refresh token can be defined here
123             If not, it will be determined during a request.
124              
125             =item * access_token <Str>
126              
127             If known the access token can be defined here.
128             If not, it will be determined during a request.
129              
130             =item * access_code <Str>
131              
132             If known, the access code can be defined here.
133             It is only necessary if you have not yet got an access/refresh token.
134             If you are running in interactive mode (and access/refresh tokens are not defined),
135             you will be given a URL to open in a browser and copy the resulting code to the command line.
136              
137             =item * token_store <Str>
138              
139             Path to a file to store your tokens.
140             This can be the same file for multiple services - it is a simple YAML file with one entry per
141             client_id which stores your refresh and access tokens.
142              
143             =item * redirect_uri <Str>
144              
145             Only needs to be defined if using the 'webserver' profile. The page to which the service provider
146             should redirect to after authorization.
147             For instances using the 'application' profile, the default 'urn:ietf:wg:oauth:2.0:oob' is used.
148              
149             =item * access_token_method <Str>
150              
151             GET or POST?
152              
153             Default: POST
154              
155             =item * bearer_token_scheme <Str>
156              
157             Should be one of: auth-header, uri-query, form-body
158            
159             Default: auth-header
160              
161             =item * profile <Str>
162              
163             Are you using this module as a webserver (users browser is forwarded to the authorization urls, and they
164             in turn redirect back to your redirect_uri), or as an application (interactively, no browser interaction
165             for authorization possible)?
166              
167             Should be one of: application, webserver
168              
169             Default: application
170              
171             =item * interactive <Bool>
172              
173             Are you running your program interactively (i.e. if necessary, do you want to have a prompt for, and paste
174             the authorization code from your browser on the command line?).
175              
176             Options: 0, 1
177              
178             Default: 1
179              
180             =item * keep_alive <Int>
181              
182             Should the LWP::UserAgent instance used have a connection cache, and how many connections should it cache?
183             Turning off keep_alive can make interaction with your service provider very slow, especially if it is
184             over an encrypted connection (which it should be).
185              
186             Default: 1 (try 2 if your service provider requires frequent authorization token refreshing)
187              
188             =item * user_agent <LWP::UserAgent>
189              
190             It is not necessary to pass a UserAgent, but maybe you have a custom crafted instance which you want to reuse...
191              
192             =item * access_token_object <Net::OAuth2::Moosey::AccessToken>
193              
194             The access token object which manages always having a fresh token ready for you.
195              
196             =back
197              
198             =cut
199              
200             has 'client_id' => ( is => 'ro', isa => 'Str', );
201             has 'client_secret' => ( is => 'ro', isa => 'Str', );
202             has 'scope' => ( is => 'ro', isa => Uri, coerce => 1, );
203             has 'site_url_base' => ( is => 'ro', isa => Uri, coerce => 1, );
204             has 'access_token_url_base' => ( is => 'ro', isa => Uri, coerce => 1, );
205             has 'authorize_url_base' => ( is => 'ro', isa => Uri, coerce => 1, );
206              
207             has 'access_token_path' => ( is => 'ro', isa => 'Str', );
208             has 'authorize_path' => ( is => 'ro', isa => 'Str', );
209             has 'refresh_token' => ( is => 'ro', isa => 'Str', );
210             has 'access_token' => ( is => 'ro', isa => 'Str', );
211             has 'access_code' => ( is => 'rw', isa => 'Str', );
212             has 'token_store' => ( is => 'ro', isa => 'Str', );
213              
214             # TODO: RCL 2011-11-03 Test if is URI if profile eq 'webserver'
215             has 'redirect_uri' => ( is => 'ro', isa => 'Str', required => 1,
216             default => 'urn:ietf:wg:oauth:2.0:oob' );
217              
218             has 'access_token_method' => ( is => 'ro', isa => 'Str', required => 1, default => 'POST' );
219             has 'bearer_token_scheme' => ( is => 'ro', isa => 'Str', required => 1, default => 'auth-header' );
220             has 'profile' => ( is => 'ro', isa => 'Str', required => 1, default => 'application' );
221             has 'interactive' => ( is => 'ro', isa => 'Bool', required => 1, default => 1 );
222             has 'keep_alive' => ( is => 'ro', isa => 'Int', required => 1, default => 1 );
223              
224             has 'user_agent' => (
225             is => 'ro',
226             isa => 'LWP::UserAgent',
227             writer => '_set_user_agent',
228             predicate => '_has_user_agent',
229             );
230              
231             has 'access_token_object' => ( is => 'rw',
232             isa => 'Net::OAuth2::Moosey::AccessToken',
233             builder => '_build_access_token_object',
234             lazy => 1,
235             );
236              
237              
238             # Create a LWP::UserAgent if necessary
239             around 'user_agent' => sub {
240             my $orig = shift;
241             my $self = shift;
242             unless( $self->_has_user_agent ){
243             $self->_set_user_agent( LWP::UserAgent->new( 'keep_alive' => $self->keep_alive ) );
244             }
245             return $self->$orig;
246             };
247              
248              
249             # Because a valid combination of parameters is not possible to define with 'has',
250             # doing a more complex param check before new
251             before 'new' => sub{
252             my $class = shift;
253             my %params = @_;
254            
255             my $found_valid = 0;
256             my @valid = (
257             [ qw/client_id client_secret site_url_base/ ],
258             [ qw/access_token no_refresh_token_ok/ ],
259             [ qw/refresh_token site_url_base/ ],
260             );
261             FOUND_VALID:
262             foreach( @valid ){
263             my @test = @{ $_ };
264             if( scalar( grep{ $params{$_} } @test ) == scalar( @test ) ){
265             $found_valid = 1;
266             last FOUND_VALID;
267             }
268             }
269             if( not $found_valid ){
270             die( "Not initialised with a valid combination of parameters...\n" . Dump( \%params ) );
271             }
272             };
273              
274             sub _build_access_token_object {
275             my $self = shift;
276            
277             # Try to load an access token from the store first
278             my $access_token = undef;
279             my %token_params = ( client => $self );
280             foreach( qw/client_id client_secret access_token access_code
281             access_token_url refresh_token token_store user_agent/ ){
282             $token_params{$_} = $self->$_ if $self->$_;
283             }
284             $access_token = Net::OAuth2::Moosey::AccessToken->new( %token_params );
285             $access_token->sync_with_store;
286             if( not $access_token->refresh_token ){
287             my $profile = $self->profile;
288              
289             # Interactive applications need to supply a code
290             if( not $self->access_code ){
291             if( $self->profile ne 'application' ){
292             croak( "access_code required but not available" );
293             }
294             printf "Please authorize your application with this URL\n%s\n",
295             $self->authorize_url();
296             if( not $self->interactive ){
297             #TODO: RCL 2011-11-02 Better handling for non-interactive. Maybe return the URL?
298             exit;
299             }
300             print "Code: ";
301             my $code = <STDIN>;
302             chomp( $code );
303             $self->access_code( $code );
304             }
305              
306             my $request;
307             if( $self->access_token_method eq 'POST' ){
308             $request = POST( $self->access_token_url(), { $self->_access_token_params() } );
309             } else {
310             $request = HTTP::Request->new(
311             $self->access_token_method => $self->access_token_url( $self->_access_token_params() ),
312             );
313             };
314            
315             my $response = $self->user_agent->request($request);
316             if( not $response->is_success ){
317             croak( "Fetch of access token failed: " . $response->status_line . ": " . $response->decoded_content );
318             }
319            
320             my $res_params = _parse_json($response->decoded_content);
321             $res_params = _parse_query_string($response->decoded_content) unless defined $res_params;
322             if( not defined $res_params ){
323             croak( "Unable to parse access token response '".substr($response->decoded_content, 0, 64)."'" );
324             }
325            
326             #TODO: RCL 2011-11-02 Check that required values returned.
327             # Write the returned values to the access token object
328             foreach my $key( keys( %{ $res_params } ) ){
329             # TODO: RCL 2011-11-02 Isn't there a has_accessor way of doing this?
330             if( $access_token->meta->has_method( $key ) ){
331             $access_token->$key( $res_params->{$key} );
332             }else{
333             warn( "Unknown key found in response parameters: $key\n" );
334             }
335             }
336             $access_token->sync_with_store;
337             }
338             return $access_token;
339             }
340              
341              
342             =head2 refresh_access_token
343              
344             Make the current access token expire, and request a fresh access token
345              
346             =cut
347             sub refresh_access_token {
348             my $self = shift;
349              
350             # Make it expire now
351             $self->access_token_object->expires_at( time() );
352              
353             # Request a fresh access token
354             $self->access_token_object->valid_access_token();
355             }
356              
357             =head2 request
358              
359             Submit a request. This is a wrapper arround a basic LWP::UserAgent->request, but adds the necessary
360             headers with the access tokens necessary for an OAuth2 request.
361              
362             =cut
363             sub request {
364             my $self = shift;
365             my ($method, $uri, $header, $content) = @_;
366             my $request = HTTP::Request->new(
367             $method => $self->_site_url($uri), $header, $content
368             );
369             # We assume a bearer token type, but could extend to other types in the future
370             my @bearer_token_scheme = split ':', $self->bearer_token_scheme;
371             if (lc($bearer_token_scheme[0]) eq 'auth-header') {
372             # Specs suggest using Bearer or OAuth2 for this value, but OAuth appears to be the de facto accepted value.
373             # Going to use OAuth until there is wide acceptance of something else.
374             my $auth_scheme = $self->access_token_object->token_type || $bearer_token_scheme[1] || 'OAuth';
375             $request->headers->push_header(Authorization => $auth_scheme . " " . $self->access_token_object->valid_access_token);
376             }
377             elsif (lc($bearer_token_scheme[0]) eq 'uri-query') {
378             my $query_param = $bearer_token_scheme[1] || 'oauth_token';
379             $request->uri->query_form($request->uri->query_form, $query_param => $self->access_token_object->valid_access_token);
380             }
381             elsif (lc($bearer_token_scheme[0]) eq 'form-body') {
382             croak "Embedding access token in request body is only valid for 'application/x-www-form-urlencoded' content type"
383             unless $request->headers->content_type eq 'application/x-www-form-urlencoded';
384             my $query_param = $bearer_token_scheme[1] || 'oauth_token';
385             $request->add_content(
386             ((defined $request->content and length $request->content) ? "&" : "") .
387             uri_escape($query_param) . '=' . uri_escape($self->valid_access_token)
388             );
389             }
390             return $self->user_agent->request( $request );
391             }
392              
393             =head2 post
394              
395             A wrapper for the request method already defining the request method as POST
396              
397             =cut
398             sub post {
399             my $self = shift;
400             $self->request( 'POST', @_ );
401             }
402              
403             =head2 get
404              
405             A wrapper for the request method already defining the request method as GET
406              
407             =cut
408             sub get {
409             my $self = shift;
410             $self->request( 'GET', @_ );
411             }
412              
413             =head2 authorize_url
414              
415             Returns the authorization url
416              
417             =cut
418             sub authorize_url {
419             my $self = shift;
420             return $self->_make_url("authorize", $self->_authorize_params( @_ ) );
421             }
422              
423             =head2 access_token_url
424              
425             Returns the access token url
426              
427             =cut
428             sub access_token_url {
429             return shift->_make_url("access_token", @_ );
430             }
431              
432              
433             # Internal method to prepare the necessary authorize parameters
434             sub _authorize_params {
435             my $self = shift;
436             my %options = @_;
437             $options{scope} ||= $self->scope;
438             $options{client_id} ||= $self->client_id;
439             $options{response_type} ||= 'code';
440             $options{redirect_uri} ||= $self->redirect_uri;
441            
442             if( $self->profile eq 'webserver' ){
443             # legacy for pre v2.09 (37Signals)
444             $options{type} = 'web_server';
445             }
446             return %options;
447             }
448              
449             # Internal method to prepare the necessary access token parameters
450             sub _access_token_params {
451             my $self = shift;
452             my %options = @_;
453             $options{client_id} ||= $self->client_id;
454             $options{client_secret} ||= $self->client_secret;
455             $options{grant_type} ||= 'authorization_code';
456             $options{code} = $self->access_code if $self->access_code;
457             $options{redirect_uri} ||= $self->redirect_uri;
458              
459             if( $self->profile eq 'webserver' ){
460             # legacy for pre v2.09 (37Signals)
461             $options{type} = 'web_server';
462             }
463            
464             return %options;
465             }
466              
467             # The URL can be put together with various information... do what you can with what you've got!
468             sub _make_url {
469             my $self = shift;
470             my $thing = shift;
471             my $path = $self->{"${thing}_url_base"} || $self->{"${thing}_path"} || "/oauth/${thing}";
472             return $self->_site_url($path, @_);
473             }
474              
475             # Internal method to return the site url built from the site_url_base
476             sub _site_url {
477             my $self = shift;
478             my $path = shift;
479             my %params = @_;
480             my $url;
481             if( $self->site_url_base ) {
482             $url = URI->new_abs($path, $self->site_url_base );
483             }
484             else {
485             $url = URI->new($path);
486             }
487             if (@_) {
488             $url->query_form($url->query_form , %params);
489             }
490             return $url;
491             }
492              
493              
494             # Parse the query string
495             sub _parse_query_string {
496             my $str = shift;
497             my $uri = URI->new;
498             $uri->query($str);
499             return {$uri->query_form};
500             }
501              
502             # Parse json non-fataly
503             sub _parse_json {
504             my $str = shift;
505             my $obj = eval{local $SIG{__DIE__}; decode_json($str)};
506             return $obj;
507             }
508              
509             1;
510             =head1 LICENSE AND COPYRIGHT
511              
512             Copyright 2011 Robin Clarke
513              
514             This program is free software; you can redistribute it and/or modify it
515             under the terms of either: the GNU General Public License as published
516             by the Free Software Foundation; or the Artistic License.
517              
518             See http://dev.perl.org/licenses/ for more information.
519              
520             =head1 CONTRIBUTORS
521              
522             Thanks to Keith Grennan for Net::OAuth2 on which this is based
523              
524             =cut
525