How do I stack Plack authentication handlers? - perl

I would like to have my Plack app try several different means of authorizing the user. Specifically, check if the user is already authorized via a session cookie, then check for Digest authentication and then fall back to Basic.
I figured I could just enable a bunch of Auth handlers in the order I wanted them to be checked (Session, Digest, Basic). Unfortunately, the way that Plack::Middleware::Auth::Digest and Plack::Middleware::Auth::Basic are written they both return 401 if digest or basic auth doesn't exist, respectively.
How is this normally dealt with in Plack?

I do not have an implementation but I think I have the approach. You can do this "in-line" with Plack::Middleware::Conditional. So it would look like this but you'll have to fill in the missing conditions/tests. I didn't see an easy/obvious way but I suspect you might. Since you have the $env to pass around you should be able to set/check HTTP_/session stuff in the order you want and keep the state for the next handler to know if it should be enabled or not.
use Plack::Builder;
my $app = sub {
[ 200,
[ "Content-Type" => "text/plain" ],
[ "O HAI, PLAK!" ]
];
};
builder {
enable "Session::Cookie";
enable_if { my $env = shift;
# I don't know...
} "Auth::Digest",
realm => "Secured", secret => "BlahBlah",
authenticator => sub { $_[0] eq $_[1] };
enable_if { my $env = shift;
# I don't know...
} "Auth::Basic",
authenticator => sub { $_[0] eq $_[1] };
$app;
};

I think you are going to need to write your own middleware, since ideally (based on a very quick read of RFC 2617) when not authenticated you would return a WWW-Authenticate header with both Basic and Digest challenges (with Basic first, for user agents that only understand Basic).

Related

How to access session data from test?

The Mojolicious framework states next:
Any aspect of the application (helpers, plugins, routes, etc.) can be introspected from Test::Mojo through the application object.
But when helper, for example, $c->current_user deals with session it fails.
The session data is not available and I can not access it from test:
$t->app->session # {}
Thus $t->app->current_user fails too.
How to access session data from test?
UPD The test
use Mojo::Base -strict;
use Mojolicious::Lite;
use Test::More;
use Test::Mojo;
get '/set_session' => sub {
my $c = shift;
$c->session->{ user_id } = 1;
$c->render( text => $c->session->{ user_id } );
};
get '/get_session' => sub {
my $c = shift;
$c->render( text => $c->session->{ user_id } );
};
my $t = Test::Mojo->new;
$t->get_ok( '/set_session' )->status_is(200);
is $t->app->session->{ user_id }, 1, 'Session available from test script';
$t->get_ok( '/get_session' )->status_is(200)
->content_is( 1 );
done_testing();
UPD test result
ok 1 - GET /set_session
ok 2 - 200 OK
not ok 3 - Session available from test script
# Failed test 'Session available from test script'
# at t/session.t line 22.
# got: undef
# expected: '1'
ok 4 - GET /get_session
ok 5 - 200 OK
ok 6 - exact match for content
1..6
# Looks like you failed 1 test of 6.
UPD
It seems that Mojo::Test object should save session object in addition to the request and response objects from the previous transaction
To test helpers in context of last request I write next role:
package Test::Mojo::Role::Helper;
use Mojo::Base -role;
sub helper {
my( $t ) = #_;
$t->tx->req->cookies( #{ $t->tx->res->cookies } );
$t->app->build_controller( $t->tx );
}
1;
Then use it as next:
use Test::Mojo;
my $t = Test::Mojo->with_roles( '+Helper' )->new( 'MyApp' );
$t->post_ok( '/login', json => { contact => $user_c, password => $user_p } )
->status_is( 200 );
is $t->helper->uid, 1, "Authorized user has identificator";
is $t->helper->role, 'user', "Authorized user has 'user' privilege";
UPD More robust solution
package Test::Mojo::Role::Helper;
use Mojo::Base -role;
my $req_context; # Here is controller object
sub helper { $req_context }
sub hook_context {
my( $t ) = #_;
$t->app->hook( after_dispatch => sub{ $req_context = shift });
$t;
}
1;
The testing is same with next small difference. When application is constructed we should hook to after_dispatch event:
my $t = Test::Mojo
->with_roles( '+Helper' )
->new( 'App' )
->hook_context;
The Test::Mojo class does not give you direct access to the session contents. The test class represents a client of your Mojolicious application, and the client does not have direct access to the session cookie either (well, it's just base64-encoded JSON so it's not exactly secret, but still …).
The “proper” way to test the session is to check that the app behaves correctly regarding the session, not just to check that the session was set to some value. That's effectively what your /get_session endpoint does. Of course you shouldn't just add such an endpoint for testing, but consider how the session fits into your requirements. E.g. as a BDD-style scenario:
Feature: the secret page
there is a secret page that should be only visible to logged-in users.
Background:
Given a user "test:test123"
Given a new client
Scenario: users cannot see the page when they are not logged in
When I visit the /secret page
Then I get a 404 response
Scenario: users can see the page after logging in
Given I log in as "test:test123"
When I visit the /secret page
Then I see "this is the secret"
The $t->app->session does not contain the session because the session data is loaded into the controller's stash. This only exists for the duration of the request. In particular app->session is merely a helper that delegates to the current controller, not a primary method of the application.
If you really need to peek into the session cookie, this might be the least insane way to do it, short of inflating a controller object:
my ($session) = grep { $_->name eq $t->app->sessions->cookie_name } $t->ua->cookie_jar->all->#*;
$session = $session->value =~ s/--[^-]+$//r; # strip signature
$session =~ tr/-/=/;
$session = $t->app->sessions->deserialize->(Mojo::Util::b64_decode $session);

Twitter LED Timeline

Hello I have put together a script for a twitter timeline it works apart from i dont know how to authorize my twitter api key my led sign is just saying "Bad Authentication data"
here is my code
#!/usr/bin/perl
require LWP::UserAgent;
use JSON;
my $lwpua = LWP::UserAgent->new;
my $uagent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6";
my #header = ( 'Referer' => 'http://api.twitter.com/', 'User-Agent' => $uagent );
my $twuser = '<twitter_name>';
my $twurl = "http://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=$twuser";
my $response = $lwpua->get( $twurl, #header );
my $return = $response->content;
my $json = JSON->new->allow_nonref;
my $json_text = $json->decode($return);
my #tweets = #{$json_text};
my $message;
foreach $tweet (#tweets) {
$message .= $tweet->{text} . "\n";
}
use Device::MiniLED;
my $sign = Device::MiniLED->new( devicetype => "sign" );
$sign->addMsg(
data => "$message",
effect => "scroll",
speed => 4
);
$sign->send( device => "/dev/ttyUSB0" );
1;
First: use strict; and use warnings;. Even if you're the awesomest programmer ever, this should be your first port of call if you're having problems. (And everyone makes typos).
Secondly: $json_text is a hash ref, not an array ref. You probably want to use values or similar.
Thirdly: Bad Authentication Data is a twitter api error, not a code error. You need to authorize it with oAuth, and you're doing no twitter auth at all. From: https://dev.twitter.com/overview/api/response-codes
215 - Typically sent with 1.1 responses with HTTP code 400. The method requires authentication but it was not presented or was wholly invalid.
E.g. you can't do what you're doing without authenticating. I think what you need is this:
https://dev.twitter.com/oauth/overview/application-owner-access-tokens
Specifically - the 'easy answer' is generate an account specific authentication token, and send that in your request.
Once you have done this this web page on Twitter:
https://dev.twitter.com/oauth/tools/signature-generator/
allows you to generate a (time limited) example command that you could use to fetch your timeline. But you'll most likely need to build authentication into your script in order to do what you're trying to do. (user_timeline.json is a restricted access API). (There's a 'test oAuth' button on the app webpage).
Reading through the docs on creating authentication tokens, makes me thing that installing Net::Twitter or Net::Twitter::Lite might be the way to go.
First follow the instructions here: https://dev.twitter.com/oauth/overview/application-owner-access-tokens
Specifically, on https://apps.twitter.com/ you need to:
create an application
generate a token (from the application page).
(Under 'keys and access tokens' on the app specific page).
This will give you the 4 things you need to speak to twitter:
a consumer key
a consumer secret
an access token
an access token secret
By default, your access token will be read only. This is fine for what you want to do.
The process for turning them into Twitter auth is a bit complicated and involves RSA-HMAC encryption. So just let Net::Twitter do it for you: (I've removed my keys, because I'm not quite daft enough to post the equivalent of my Twitter password)
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use Net::Twitter;
my $twitter = Net::Twitter->new(
traits => [qw/API::RESTv1_1/],
consumer_key => 'long_string_of_stuff',
consumer_secret =>
'long_string_of_stuff',
access_token => '12345-long_string_of_stuff',
access_token_secret =>
'long_string_of_stuff',
ssl => 1,
);
my $tweets = $twitter->user_timeline();
print Dumper \$result;
my $message;
foreach my $tweet ( #{$tweets} ) {
$message .= $tweet->{text} . "\n";
}
print $message;
Tested this with my account, and it prints a list of my recent tweets, which I think was what you wanted?

Less verbose debug screen in Catalyst?

in my stage server I would like to activate the debug so the clients can find errors for themselves before the app goes to the production server.
BUT I only want the first part of the message, not the Request, or the Session Data.
For example: Couldn't render template "templates/home.tt2: file error - templates/inc/heater: not found".
The message is enough for me and for my client to see that the "header" call is misspelled.
The Request has a lot of irrelevant information for the client, but also has A LOT of internal developing information that should be hidden all the time!!
Regards
What you want is to override Catalyst's dump_these method. This returns a list of things to display on Catalyst's error debugging page.
The default implementation looks like:
sub dump_these {
my $c = shift;
[ Request => $c->req ],
[ Response => $c->res ],
[ Stash => $c->stash ],
[ Config => $c->config ];
}
but you can make it more restrictive, for example
sub dump_these {
my $c = shift;
return [ Apology => "We're sorry that you encountered a problem" ],
[ Response => substr($c->res->body, 0, 512) ];
}
You would define dump_these in your app's main module -- the one where you use Catalyst.
I had a similar problem that I solved by overriding the Catalyst method log_request_parameters.
Something like this (as #mob said, put it in your main module):
sub log_request_parameters {
my $c = shift;
my %all_params = #_;
my $copy = Clone::clone(\%all_params); # don't change the 'real' request params
# Then, do anything you want to only print what matters to you,
# for example, to hide some POST parameters:
my $body = $copy->{body} || {};
foreach my $key (keys %$body) {
$body->{$key} = '****' if $key =~ /password/;
}
return $c->SUPER::log_request_parameters( %$copy );
}
But you could also simply return at the beginning, if you don't want any GET/POST parameters displayed.
Well, I didn't think of the more obvious solution, in your case: you could simply set your log level to something higher than debug, which would prevent these debug logs from being displayed, but would keep the error logs:
# (or a similar condition to check you are not on the production server)
if ( !__PACKAGE__->config->{dev} ) {
__PACKAGE__->log->levels( 'warn', 'error', 'fatal' ) if ref __PACKAGE__->log;
}

Understanding oAuth with Perl

i have a problem making simple API request to the Yammer (https://www.yammer.com/api_doc.html). I need to get https://www.yammer.com/api/v1/groups.xml (Groups: A list of groups).
I'm trying to use Net::OAuth::Simple. Here is my Yammer.pm:
package Yammer;
use strict;
use base qw(Net::OAuth::Simple);
sub new {
my $class = shift;
my %tokens = #_;
return $class->SUPER::new( tokens => \%tokens,
urls => {
authorization_url => "https://www.yammer.com/oauth/authorize",
request_token_url => "https://www.yammer.com/oauth/request_token",
access_token_url => "https://www.yammer.com/oauth/access_token",
},
protocol_version => '1.0a',
);
}
sub view_restricted_resource {
my $self = shift;
my $url = shift;
return $self->make_restricted_request( $url, 'GET' );
}
sub update_restricted_resource {
my $self = shift;
my $url = shift;
my %extra_params = #_;
return $self->make_restricted_request($url, 'POST', %extra_params);
}
1;
And here is my main program:
use Yammer;
# Get the tokens from the command line, a config file or wherever
my %tokens = (
consumer_key => 'Baj7MciMhmnDTwj6kaOV5g',
consumer_secret => 'ejFlGBPtXwGJrxrEnwGvdRyokov1ncN1XxjmIm34M',
callback => 'https://www.yammer.com/oauth/authorize',
);
my $app = Yammer->new(%tokens);
# Check to see we have a consumer key and secret
unless ($app->consumer_key && $app->consumer_secret) {
die "You must go get a consumer key and secret from App\n";
}
# If the app is authorized (i.e has an access token and secret)
# Then look at a restricted resourse
if ($app->authorized) {
my $response = $app->view_restricted_resource;
print $response->content."\n";
exit;
}
# Otherwise the user needs to go get an access token and secret
print "Go to " . $app->get_authorization_url( callback => 'https://www.yammer.com/oauth/authorize?rand=' . rand() ) . "\n";
print "Then hit return after\n";
<STDIN>;
my ($access_token, $access_token_secret) = $app->request_access_token($_);
I'm getting messages like
Go to
https://www.yammer.com/oauth/authorize?oauth_token=2sxBkKW1F1iebF2TT5Y7g&callback=https%3A%2F%2Fwww.yammer.com%2Foauth%2Fauthorize%3Frand%3D0.0045166015625
And authorizing application on this URL. After that i see message like:
You have successfully authorized the
following application: 2GIS_yammer
To complete the authorization go back
to the 2GIS_yammer application and
enter the following code:
869A
But what next? Where i must enter this number? How to perform request i need?
Thanks.
Roman
probably the number that you get after the authorization step is the oauth_verifier string that needs to be sent along with REQUEST token in order to get ACCESS token.
This is mandatory part of oAuth 1.0a implementations (which I think is the most common implementation used now, because 2.0 is still a draft and there aren't many libraries that implement it).
I guess that you don't send callback URL to the provider, and he doesn't know where to redirect the user after authorization. When the provider doesn't know a callback URL, he cannot redirect the user back to your (consumer) application.
In that case the specification says that it should print the verifier string on the screen, so you (the user) can take it manually and give it to your (consumer) application , and so to build the request for ACCESS TOKEN.
If you DO provide callback URL (in your first request for REQUEST token), then most probably you will not get the screen with this number, but instead, you (the user) will be redirected to the callback URL with it automatically.
E.g. if your callback url is: http://myapp.com/oauth/callback, then the provider will redirect the user to your callback url with proper values in the query string.
redirect: http://myapp.com/oauth/callback?oauth_token=xxxx&oauth_verifier=yyyy
Then your application should take the verifier string and add it as a parameter to the request for ACCESS TOKEN (as you have done previously with the other parameters like nonce, timestamp, oauth_token, etc.)
As a response to this last request (with oauth_verifier string included) you should get ACCESS TOKEN.
Here is a good explanation about the oauth_verifier string and why it was introduced in the protocol:
http://hueniverse.com/2009/04/explaining-the-oauth-session-fixation-attack/

How can I log in to YouTube using Perl?

I am trying to write a Perl script to connect to me YouTube account but it doesnt seem to work. Basically I just want to connect to my account but apparently it is not working. I don't even have an idea on how I could debug this! Maybe it is something related to https protocol?
Please enlighten me! Thanks in advance.
use HTTP::Request::Common;
use LWP::UserAgent;
use strict;
my $login="test";
my $pass = "test";
my $res = "";
my $ua = "";
# Create user agent, make it look like FireFox and store cookies
$ua = LWP::UserAgent->new;
$ua->agent("Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.12) Gecko/20051213 Firefox/1.0.7");
$ua->cookie_jar ( {} );
# Request login page
$res = $ua->request(GET "https://www.google.com/accounts/ServiceLogin?service=youtube&hl=en_US&passive=true&ltmpl=sso&uilel=3&continue=http%3A//www.youtube.com/signup%3Fhl%3Den_US%26warned%3D%26nomobiletemp%3D1%26next%3D/index");
die("ERROR1: GET http://www.youtube.com/login\n") unless ($res->is_success);
# Now we login with our user/pass
$res = $ua->request(
POST "https://www.google.com/accounts/ServiceLoginAuth?service=youtube",
Referer => "http://www.youtube.com/login",
Content_Type => "application/x-www-form-urlencoded",
Content => [
currentform => "login",
next => "/index",
username => $login,
password => $pass,
action_login => "Log+In"
]
);
# YouTube redirects (302) to a new page when login is success
# and returns OK (200) if the login failed.
#die("ERROR: Login Failed\n") unless ($res->is_redirect());
print $res->content;
what i am doing is learning the web features of perl, so i dont want to use any library except wwwlib or mechanize to get the job done.
how can i just connect to my account using a perl script? this is my objective for now
hope someone can post a script or correct mine.
thank you guys for you help.
i am testing Webscarab now..
What data are you trying to grab? Why not just using an existing implementation like WebService::YouTube
Some comments on your code: I always avoided the shortcut $ua->request(GET/POST) method since I always ended up needing more flexibility that only the use of HTTP::Request and HTTP::Response allowed. I always felt the code was cleaner that way too.
Why is your code not working? Who knows.
Make sure your cookiejar is adding your cookies to the outgoing HTTP::Request. I'd suggest dumping all your headers when you do it in a browser and compare with the headers and data that libwww is sending. There may be some additional fields that they are checking for that vary for every hit. They may be checking for your UserAgent string. If you are just looking to learn libwww I'd suggest using a different site as a target as I'm sure YouTube has all sort of anti-scripting hardening.
Are you using YouTube's stable documented API?
Use an HTTP proxy such as WebScarab to watch the data flow.
Trey's suggestion to use somebody else's CPAN package for the mechanics is a good idea too.
Right right by and large, what you want to do is define a cookiejar for most of these websites that have a redirection login. This is what the package has done. Also the package tunes a lot of the lookups and scrapes based on the youtube spec.
Ajax content for example will be rough since its not there when your scraping
You just picked a somewhat rough page to start out with.
Enjoy
I'm actually working on this issue myself. Before, I would suggest read over this the API guide from Google as a good starting reference. If I'm reading it correctly, one begins with passing user credentials through a REST interface to get a Authentication Token. To handle that, I'm using the following:
sub getToken {
my %parms = #_;
my $response = LWP::UserAgent->new->post(
'https://www.google.com/youtube/accounts/ClientLogin',
[
Email => $parms{'username'},
Passwd => $parms{'password'},
service => "youtube",
source => "<<Your Value Here>>",
]
);
my $content = $response->content;
my ($auth) = $content =~ /^Auth=(.*)YouTubeUser(.*)$/msg
or die "Unable to authenticate?\n";
my ($user) = $content =~ /YouTubeUser=(.*)$/msg
or die "Could not extract user name from response string. ";
return ($auth, $user);
}
And I call that from the main part of my program as such:
## Get $AuthToken
my ($AuthToken, $GoogleUserName) = getToken((
username => $email, password => $password
));
Once I have these two things -- $AuthToken and $GoogleUserName, I'm still testing the LWP Post. I'm still writing this unit:
sub test {
my %parms = #_;
## Copy file contents. Use, foy's three param open method.
my $fileSize = -s $parms{'File'};
open(VideoFile, '<', "$parms{'File'}") or die "Can't open $parms{'File'}.";
binmode VideoFile;
read(VideoFile, my $fileContents, $fileSize) or die "Can't read $parms{'File'}";
close VideoFile;
my $r = LWP::UserAgent->new->post(
"http://uploads.gdata.youtube.com/feeds/api/users/$parms{'user'}/uploads",
[
Host => "uploads.gdata.youtube.com",
'Authorization' => "AuthSub token=\"$parms{'auth'}\"",
'GData-Version' => "2",
'X-GData-Key' => "key=$YouTubeDeveloperKey",
'Slug' => "$parms{'File'}",
'Content-Type' => "multipart/related; boundary=\"<boundary_string>\"",
'Content-Length' => "<content_length>",
'video_content_type'=> "video/wmv",
'Connection' => "close",
'Content' => $fileContents
]
);
print Dumper(\$r->content)
}
And that is called as
&test((auth=>$Auth, user=>$user, File=>'test.wmv'));