The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Plack::Middleware::OAuth;
use warnings;
use strict;
use parent qw(Plack::Middleware);
use DateTime;
use Digest::MD5 qw(md5_hex);
use Plack::Util::Accessor qw(providers on_success on_error debug);
use Plack::Session;
use Plack::Response;
use Plack::Request;
use URI;
use URI::Query;
use Plack::Middleware::OAuth::UserInfo;
use Plack::Middleware::OAuth::Handler::RequestTokenV1;
use Plack::Middleware::OAuth::Handler::RequestTokenV2;
use Plack::Middleware::OAuth::Handler::AccessTokenV1;
use Plack::Middleware::OAuth::Handler::AccessTokenV2;
use DateTime;
use feature qw(switch say);

our $VERSION = '0.10';

# routes cache
#    path => { provider => ... , method => .... }
our %routes;


sub version1_required {
    qw(consumer_key consumer_secret request_token_url access_token_url request_token_method access_token_method signature_method);
}

sub version2_required {
    qw(client_id client_secret authorize_url access_token_url);
}


sub arguments_checking {
    my ($self,$provider_name,$config) = @_;
    # version 1 checking
    given ( $config->{version} ) {
        when(2) {  
            for( $self->version2_required ) { die "Please setup $_ for $provider_name" unless $config->{$_}; }
        }
        when(1) {
            for( $self->version1_required ) { die "Please setup $_ for $provider_name" unless $config->{$_}; }
        }
    }
}

sub load_config_from_pkg {
    my ($self,$provider_name) = @_;
    my $class = __PACKAGE__ . '::' . $provider_name;
    $class = Plack::Util::load_class( $class );
    return $class->config( $self );
}

sub prepare_app {
	my $self = shift;
	my $p = $self->providers;

    unless( ref($p) ) {
        if( $p =~ /\.yml$/ ) {
            use YAML::Any;
            say STDERR "Loading Provider YAML File: $p";
            $p = YAML::Any::LoadFile( $p );
            $self->providers( $p );
        }
    }

	for my $provider_name ( keys %$p ) {
		my $config = $p->{$provider_name};

		my $fc = ord(substr( $provider_name , 0 , 1 ));
		if( $fc >= 65 && $fc <= 90 ) {
			my $default_config = $self->load_config_from_pkg( $provider_name );
			for my $k ( keys %$default_config ) {
				$config->{ $k } ||= $default_config->{ $k };
			}
		}

		$config->{signature_method} ||= 'HMAC-SHA1';
		$config->{version} ||= 1;

        $self->arguments_checking( $provider_name , $config );

		# mount routes
		my $path = '/' . lc( $provider_name );
		my $callback_path = '/' . lc( $provider_name ) . '/callback';

        say STDERR "[OAuth] Mounting $provider_name to $path ...";
		$self->add_route( $path , { provider => $provider_name , method => 'request_token' } );

        say STDERR "[OAuth] Mounting $provider_name callback to $callback_path ...";
		$self->add_route( $callback_path , { provider => $provider_name , method => 'access_token' } );
	}
}

sub get_provider_names { 
	my $self = shift;
	return keys %{ $self->providers };
}

sub add_route { 
	my ($self,$path,$config) = @_;
	$routes{ $path } = $config;
}

sub dispatch_oauth_call { 
	my ($self,$env) = @_;
	my $path = $env->{PATH_INFO};
	my $n = $routes{ $path };
	return unless $n;
	my $method = $n->{method};
	return $self->$method( $env , $n->{provider} );
}

sub call {
	my ($self,$env) = @_;
	my $res;
	$res = $self->dispatch_oauth_call( $env );
	return $res if $res;

	$res = $self->app->( $env );
	return $res;
}

sub request_token {
	my ($self,$env,$provider) = @_;  # env and provider id
	my $config = $self->providers->{ $provider };
    my $class;
    given( $config->{version} ) {
        when (2) { $class = 'Plack::Middleware::OAuth::Handler::RequestTokenV2' }
        default  { $class = 'Plack::Middleware::OAuth::Handler::RequestTokenV1' }
    }

    my $req = $class->new( $env );
    $req->provider( $provider );
    $req->config( $config );
    return $req->run();
}


# Access token methods ....
sub access_token {
	my ($self,$env,$provider) = @_;
	my $config = $self->providers->{ $provider };

    my $class;
    given( $config->{version} ) {
        when (2) { $class = 'Plack::Middleware::OAuth::Handler::AccessTokenV2' }
        default  { $class = 'Plack::Middleware::OAuth::Handler::AccessTokenV1' }
    }

    my $req = $class->new( $env );
    $req->on_success( $self->on_success );
    $req->on_error( $self->on_error );
    $req->provider( $provider );
    $req->config( $config );
    return $req->run();
}

1;
__END__

=head1 NAME

Plack::Middleware::OAuth - Plack middleware for OAuth1, OAuth2 and builtin provider configs. 

=head1 DESCRIPTION

This module is still in B<**BETA**> , B<DO NOT USE THIS FOR PRODUCTION!>

L<Plack::Middleware::OAuth> supports OAuth1 and OAuth2, and provides builtin config for providers like Twitter, GitHub, Google, Facebook.
The only one thing you need to mount your OAuth service is to setup your C<consumer_key>, C<consumer_secret> (for OAuth1) or C<client_id>, C<client_secret>, C<scope> (for OAuth2).

This middleware also generates authorize url (mount_path/provider_id) and auththorize callback url (mount_path/provider_id/callback). 
If the authorize path matches, then user will be redirected to OAuth provider to authorize your application.

For example, if you mount L<Plack::Middleware::OAuth> on F</oauth>, then you can access L<http://youdomain.com/oauth/twitter> to authorize,
L<Plack::Middleware::OAuth> will redirect you to Twitter, after authorized, then Twitter will redirect you to your callback url
L<http://youdomain.com/oauth/twitter/callback>.

For more details, please check the example psgi in F<eg/> directory.

=head1 SYNOPSIS

	use Plack::Builder;

	builder {

        mount '/oauth' => builder {
            enable 'OAuth', 

                on_success => sub  { 
                    my ($self,$token) = @_;
                    my $env = $self->env;

                    my $config = $self->config;   # provider config


                    return $self->render( '..html content..' );
                    return $self->redirect( .... URL ... );

                    return [  200 , [ 'Content-type' => 'text/html' ] , 'Signin!' ];


                },

                on_error => sub {  ...  },

                providers => 'providers.yml',   # also works

                providers => {

                    # capital case implies Plack::Middleware::OAuth::Twitter
                    'Twitter' =>
                    {
                        consumer_key      => ...
                        consumer_secret   => ...
                    },

                    # captical case implies Plack::Middleware::OAuth::Facebook
                    'Facebook' =>
                    {
                        client_id        => ...
                        client_secret           => ...
                        scope            => 'email,read_stream',
                    },

                    'GitHub' => 
                    {
                        client_id => ...
                        client_secret => ...
                        scope => 'user,public_repo'
                    },

                    'Google' =>  { 
                        client_id     => '',
                        client_secret => '',
                        scope         => 'https://www.google.com/m8/feeds/'
                    },
                    
                    'Live' =>  { 
                        client_id     => '',
                        client_secret => '',
                        scope         => 'wl.basic'
                    },

                    'custom_provider' => { 
                        version => 1,
                        ....
                    }
			};
        };
		$app;
	};

The callback/redirect URL is set to {SCHEMA}://{HTTP_HOST}/{prefix}/{provider}/callback by default.

=head1 OAuth URL and Callback URL

For a defined key in providers hashref, and you mounted OAuth middleware at F</oauth>, 
the generated URLs will be like:

    authorize path: /oauth/custom_provider
    authorize callback path: /oauth/custom_provider/callback

The provider id (key) will be converted into lower-case.

For example, GitHub's URLs will be like:

    /oauth/github
    /oauth/github/callback

Facebook,

    /oauth/facebook
    /oauth/facebook/callback

You can also specify custom callback URL in a provider config.


=head1 Specify Success Callback

When access token is got, success handler will be called: 

    enable 'OAuth', 
        providers => { .... },
        on_success => sub  { 
            my ($self,$token) = @_;

            # $self: Plack::Middleware::OAuth::Handler (isa Plack::Request) object

            return $self->render( .... );

            return $self->redirect( .... );

            return $self->to_yaml( .... );

            return $self->to_json( .... );

            # or just return a raw arrayref
            return [  200 , [ 'Content-type' => 'text/html' ] , 'Signin!' ];
        };

Without specifying C<on_success>, OAuth middleware will use YAML to dump the response data to page.

To use access token to get user information, the following example demonstracte how to get corresponding user information:

    on_success => sub {
        my ($self,$token) = @_;

        if( $token->is_provider('Twitter') ) {
            my $config = $self->config;

            # return $self->to_yaml( $config );

            # get twitter user infomation with (api)
            my $twitter = Net::Twitter->new(
                traits              => [qw/OAuth API::REST/],
                consumer_key        => $config->{consumer_key},
                consumer_secret     => $config->{consumer_secret},
                access_token        => $token->access_token,
                access_token_secret => $token->access_token_secret,
            );

            return $self->to_yaml( { 
                account_settings => $twitter->account_settings,
                account_totals => $twitter->account_totals,
                show_user => $twitter->show_user( $token->params->{extra_params}->{screen_name} )
            } );
        }
    }

=head1 User Info Query Interface

To query user info from OAuth provider, you can use L<Plack::Middleware::OAuth::UserInfo> to help you.

    my $userinfo = Plack::Middleware::OAuth::UserInfo->new( 
        token =>  $token , 
        config => $provider_config
    );
    my $info_hash = $userinfo->ask( 'Twitter' );   # load Plack::Middleware::OAuth::UserInfo::Twitter

In you oauth success handler, it would be like:

    on_success => sub {
        my ($self,$token) = @_;

        my $userinfo = Plack::Middleware::OAuth::UserInfo->new( 
            token =>  $token , 
            config => $self->config
        );
        my $info_hash = $userinfo->ask( 'Twitter' );   # load Plack::Middleware::OAuth::UserInfo::Twitter
        return $self->to_yaml( $info_hash );
    };

=head1 Error Handler

An error handler should return a response data, it should be an array reference, for be finalized from L<Plack::Response>:

    enable 'OAuth', 
        providers => { .... },
        on_error => sub {
            my ($self,$token) = @_;

            $self->render( 'Error' ) unless $token;

            # $self: Plack::Middleware::OAuth::Handler (isa Plack::Request) object

        };

=head1 OAuth1 AccessToken Callback Data Structure

Twitter uses OAuth 1.0a, and the access token callback returns data like this:

    ---
    params:
        access_token: {{string}}
        access_token_secret: {{string}}
        extra_params:
            screen_name: {{screen name}}
            user_id: {{user id}}
    provider: Twitter
    version: 1


=head1 OAuth2 AccessToken Callback Data Structure

GitHub uses OAuth 2.0, and the access token callback returns data like this:

    ---
    params:
        code: {{string}}
        access_token: {{string}}
        token_type: bearer
    provider: GitHub
    version: 2

Google returns:

    ---
    params:
        access_token: {{string}}
        code: {{string}}
        expires_in: 3600
        refresh_token: {{string}}
        token_type: Bearer
    provider: Google
    version: 2

=head1 Sessions

You can get OAuth1 or OAuth2 access token from L<Plack::Session>,

    my $session = Plack::Session->new( $env );
    $session->get( 'oauth.twitter.access_token' );
    $session->get( 'oauth.twitter.access_token_secret' );  # OAuth version

    $session->get( 'oauth.facebook.access_token' );
    $session->get( 'oauth.facebook.version' );   # OAuth version

Custom provider:

    $session->get( 'oauth.custom_provider.access_token' );
    $session->get( 'oauth.custom_provider.version' );


=head1 Supported Providers

=for 4

=item

Google

=item

Twitter

=item

Facebook

=item

GitHub

=item

Live

=back

=head1 See Also

L<Net::OAuth>, L<Net::OAuth2>

=head1 Reference

=for 4

=item *

OAuth Workflow 
L<http://hueniverse.com/oauth/guide/workflow/>

=item *

OAuth 2.0 Protocal Draft
L<http://tools.ietf.org/html/draft-ietf-oauth-v2>

=item *

GitHub - Create A New Client
L<https://github.com/account/applications>

=item *

Twitter - Using OAuth 1.0a
L<https://dev.twitter.com/docs/auth/oauth>

=item *

Twitter - Moving from Basic Auth to OAuth
L<https://dev.twitter.com/docs/auth/moving-from-basic-auth-to-oauth>

=item *

Single-user OAuth with Examples
L<https://dev.twitter.com/docs/auth/oauth/single-user-with-examples>

=item *

Twitter - Create A New App
L<https://dev.twitter.com/apps>

=item *

Facebook OAuth
L<http://developers.facebook.com/docs/authentication/>

=item *

Facebook - Create A New App
L<https://developers.facebook.com/apps>

=item *

Facebook - Permissions
L<http://developers.facebook.com/docs/reference/api/permissions/>

=item *

Facebook - How to handle expired access_token
L<https://developers.facebook.com/blog/post/500/>

=item *

Google OAuth
L<http://code.google.com/apis/accounts/docs/OAuth2.html>

=item *

Google OAuth Scope:
L<http://code.google.com/apis/gdata/faq.html#AuthScopes>

=item *

Live OAuth
L<http://msdn.microsoft.com/en-us/library/hh243647.aspx>

=item *

Live OAuth Scope:
L<http://msdn.microsoft.com/en-us/library/hh243646.aspx>

=back

=head2 Contributors

RsrchBoy

=cut