#!/usr/bin/env perl package TestApp::Plugin::OAuth::Test; use strict; use warnings; use base qw/Jifty::Test/; use MIME::Base64; use Digest::HMAC_SHA1 'hmac_sha1'; use Jifty::Test::WWW::Mechanize; our @EXPORT = qw($timestamp $url $umech $cmech $pubkey $seckey $token_obj $server $URL response_is sign get_latest_token allow_ok deny_ok _authorize_request_token get_request_token get_authorized_token get_access_token has_rsa rsa_skip start_server); our $timestamp = 0; our $url; our $umech; our $cmech; our $pubkey = slurp('t/t/id_rsa.pub'); our $seckey = slurp('t/t/id_rsa'); our $token_obj; our $server; our $URL; our $can_write; sub setup { my $class = shift; $class->SUPER::setup; $class->export_to_level(1); } sub start_server { $server = Jifty::Test->make_server; $URL = $server->started_ok; $umech = Jifty::Test::WWW::Mechanize->new(); $cmech = Jifty::Test::WWW::Mechanize->new(); $url = $URL . '/oauth/request_token'; } sub response_is { ++$timestamp; my %params = ( oauth_timestamp => $timestamp, oauth_nonce => scalar(reverse $timestamp), oauth_signature_method => 'HMAC-SHA1', oauth_version => '1.0', code => 400, testname => "", method => 'POST', token_secret => '', params_in => 'method', @_, ); local $url = $URL . delete $params{url} if $params{url}; for (grep {!defined $params{$_}} keys %params) { delete $params{$_}; } my $code = delete $params{code}; my $testname = delete $params{testname} || "Response was $code"; my $no_token = delete $params{no_token}; my $method = delete $params{method}; my $params_in = delete $params{params_in}; my $token_secret = delete $params{token_secret}; my $consumer_secret = delete $params{consumer_secret} or die "consumer_secret not passed to response_is!"; if ($url =~ /access_token/) { $token_secret ||= $token_obj->secret; $params{oauth_token} ||= $token_obj->token; } $params{oauth_signature} ||= sign($method, $token_secret, $consumer_secret, %params); my $r; if ($params_in eq 'authz') { $cmech->default_header("Authorization" => authz(%params)); } if ($method eq 'GET') { my $query = join '&', map { "$_=" . Jifty->web->escape_uri($params{$_}||'') } keys %params; my $params = $params_in eq 'method' ? "?$query" : ''; $r = $cmech->get("$url$params"); } else { my $req = HTTP::Request->new( uc($method) => $url, ); if ($params_in eq 'method') { # avoid Encode complaining about undef for (values %params) { defined or $_ = ''; } my $content = Jifty->web->query_string(%params); $req->header('Content-type' => 'application/x-www-form-urlencoded'); $req->content($content); } $r = $cmech->request($req); } $cmech->default_headers->remove_header("Authorization"); local $Test::Builder::Level = $Test::Builder::Level + 1; main::is($r->code, $code, $testname); if ($url =~ /oauth/) { undef $token_obj; get_latest_token(); if ($no_token || $code != 200) { main::ok(!$token_obj, "Did not get a token"); } elsif ($code == 200) { main::ok($token_obj, "Successfully loaded a token object with token ".$token_obj->token."."); } } return $cmech->content; } # creates an Authorization header sub authz { my %params = @_; return "OAuth " . join ', ', map { $_ . q{="} . Jifty->web->escape_uri($params{$_}) . q{"} } keys %params; } sub sign { my ($method, $token_secret, $consumer_secret, %params) = @_; local $url = delete $params{sign_url} || $url; my $key = delete $params{signature_key}; my $sig_method = $params{oauth_signature_method} || delete $params{_signature_method}; delete $params{oauth_signature}; if ($sig_method eq 'PLAINTEXT') { my $signature = join '&', map { Jifty->web->escape_uri($_||'') } $consumer_secret, $token_secret; return $signature; } my $normalized_request_parameters = join '&', map { "$_=" . Jifty->web->escape_uri($params{$_}||'') } sort keys %params; my $signature_base_string = join '&', map { Jifty->web->escape_uri($_||'') } uc($method), $url, $normalized_request_parameters; my $signature; if ($sig_method eq 'RSA-SHA1') { require Crypt::OpenSSL::RSA; my $pubkey = Crypt::OpenSSL::RSA->new_private_key($key); $signature = encode_base64($pubkey->sign($signature_base_string), ""); } elsif ($sig_method eq 'HMAC-SHA1') { my $key = join '&', map { Jifty->web->escape_uri($_||'') } $consumer_secret, $token_secret; my $hmac = Digest::HMAC_SHA1->new($key); $hmac->add($signature_base_string); $signature = encode_base64($hmac->digest, ''); } return ($signature, $signature_base_string, $normalized_request_parameters) if wantarray; return $signature; } sub has_rsa { eval { require Crypt::OpenSSL::RSA; 1 } } sub rsa_skip { my $count = shift || Carp::carp "You must specify a number of tests to skip."; ::skip 'Crypt::OpenSSL::RSA is required for these tests', $count unless has_rsa; } sub slurp { no warnings 'once'; my $file = shift; local $/; local @ARGV = $file; my $contents = scalar <> or die "Unable to slurp $file"; return $contents; } sub get_latest_token { my $content = $cmech->content; $content =~ s/\boauth_token=(\w+)// or return; my $token = $1; $content =~ s/\boauth_token_secret=(\w+)// or return; my $secret = $1; local $Test::Builder::Level = $Test::Builder::Level + 1; main::is($content, '&', "the output was exactly oauth_token=...&oauth_secret=..."); my $package = 'Jifty::Plugin::OAuth::Model::'; if ($cmech->uri =~ /request_token/) { $package .= 'RequestToken'; } elsif ($cmech->uri =~ /access_token/) { $package .= 'AccessToken'; } else { Jifty->log->error("Called get_latest_token, but I cannot grok the URI " . $cmech->uri); return; } $token_obj = $package->new(current_user => Jifty::CurrentUser->superuser); $token_obj->load_by_cols(token => $token); if (!$token_obj->id) { Jifty->log->error("Could not find a $package with token $token"); return; } return $token_obj; } sub allow_ok { local $Test::Builder::Level = $Test::Builder::Level + 1; my $error = _authorize_request_token('Allow'); ::fail($error), return if $error; my $name = $token_obj->consumer->name; if ($can_write) { $umech->content_contains("Allowing $name to read and write your data for 1 hour."); } else { $umech->content_contains("Allowing $name to read your data for 1 hour."); } } sub deny_ok { local $Test::Builder::Level = $Test::Builder::Level + 1; my $error = _authorize_request_token('Deny'); ::fail($error), return if $error; my $name = $token_obj->consumer->name; $umech->content_contains("Denying $name the right to access your data."); } sub _authorize_request_token { local $Test::Builder::Level = $Test::Builder::Level + 1; my $which_button = shift or die "You did not specify a button to click to _authorize_request_token"; my $token = shift || $token_obj->token; $token = $token->token if ref $token; $umech->get('/oauth/authorize') or return "Unable to navigate to /oauth/authorize";; $umech->content =~ /If you trust this application/ or return "Content did not much qr/If you trust this application/"; my $moniker = $umech->moniker_for('TestApp::Plugin::OAuth::Action::AuthorizeRequestToken') or return "Unable to find moniker for AuthorizeRequestToken"; $umech->fill_in_action($moniker, token => $token, can_write => $can_write, ) or return "Unable to fill in the AuthorizeRequestToken action"; $umech->click_button(value => $which_button) or return "Unable to click $which_button button"; return; } sub get_request_token { local $Test::Builder::Level = $Test::Builder::Level + 1; response_is( url => '/oauth/request_token', code => 200, testname => "200 - plaintext signature", consumer_secret => 'bar', oauth_consumer_key => 'foo', oauth_signature_method => 'PLAINTEXT', @_, ); return $token_obj; } sub get_authorized_token { local $Test::Builder::Level = $Test::Builder::Level + 1; get_request_token(@_); allow_ok(); return $token_obj; } sub get_access_token { local $Test::Builder::Level = $Test::Builder::Level + 1; get_authorized_token() unless shift; response_is( url => '/oauth/access_token', code => 200, testname => "200 - plaintext signature", consumer_secret => 'bar', oauth_consumer_key => 'foo', oauth_signature_method => 'PLAINTEXT', ); } 1;