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);
Related
I am trying to accomplish the following.
I have a Moose style modules A and B
A need metadata as mandatory params
B wants to create the object of A multiple time
hence wanted to set as an attribute
Is there a better way to do this (so that I can pass the metadata to package A and in package B avoid calling new multiple times) also trying to get it done 1 liner if possible.
package A {
use Moose;
has 'metadata' => (
is => 'rw',
isa => 'HashRef',
default => sub {{}},
required => 1
);
sub process {
die unless keys %{shift->metadata};
# ... process
print "Success!\n";
}
__PACKAGE__->meta->make_immutable;
}
#######B#########
package B {
use Moose;
use A;
has 'obj_a' => (
is => 'rw',
isa => 'A',
writer => 'set_meta',
);
sub _set_meta {
my ( $self, $metadata) = #_;
return $self->set_meta(A->new(metadata => $metadata));
}
sub obj_with_meta {
my ( $self, $metadata) = #_;
return A->new(metadata => $metadata);
}
__PACKAGE__->meta->make_immutable;
1;
}
############
use B;
my $b = B->new();
# want to call like this but I am sure I am missing something which moose is providing
# here I am supposed to call obj_a instead of _set_meta I believe
#calling _set_meta I am bypassing the Moose attribute I guess
$b->_set_meta({id=>'id for metadata'})->process;
#works
$b->obj_with_meta({id=>'id for metadata'})->process;
Note above code is working
output is
Success!
Success!
I am trying to know if there is anything in moose that I can leverage. so that I can share data to the next class by writing to meta may be or using some trait maybe.
package A is the catalyst controller
package B is an independent module not tightly coupled with the catalyst.
Separating business logic from your controllers in a Catalyst app is a great idea. You can encapsulate it into its own modules and use them via a thin Catalyst::Model layer.
You don't actually need to worry about passing the session in from the controller, because all Catalyst::Components provide you with a means to do this, called ACCEPT_CONTEXT. This is a method that you can implement in any component, but typically it's used in models. It is called whenever a $c->model(...) call is done, and it gets passed the context object $c, and is supposed to return an object that can be used like a model. This might or might not be a Catalyst::Component object.
I've build a sample application that I will be using for this answer. You can find the full source code in this github repository.
Let's assume there is a Catalyst::Model class called MyApp::Model::API::User, with the following code. It inherits from Catalyst::Model::DBI in order to leverage database handle caching via Catalyst.
package MyApp::Model::API::User;
use strict;
use warnings;
use API::User;
use parent 'Catalyst::Model::DBI';
sub ACCEPT_CONTEXT {
my ( $self, $c, #args ) = #_;
$c->log->debug( sprintf 'Creating a new API::User object for %s line %d',
( caller(2) )[ 0, 2 ] );
return API::User->new(
dbh => $self->dbh,
metadata => $c->session->{data},
);
}
1;
Every time a Controller does $c->model('API::User') the ACCEPT_CONTEXT method gets called, and it instantiates a class called API::User, which is my implementation of your Catalyst-agnostic business logic. It accepts a database handle object, which the DBI Model provides for us, as well as the metadata, which we take from the user's session.
In my example I've made the user's ID part of the session so that there is actual metadata to play with (and if there is none, we create one, but that's not important here).
package API::User;
use Moose;
use DBI;
has metadata => (
isa => 'HashRef',
is => 'ro',
required => 1, # either it's required or it has a default
);
has dbh => (
isa => 'DBI::db',
is => 'ro',
required => 1,
);
sub create { ... }
sub read {
my ($self) = #_;
my $sql = 'SELECT id, number_of_writes FROM user WHERE id=?';
my $sth = $self->dbh->prepare($sql);
$sth->execute( $self->metadata->{id} );
return $sth->fetchrow_hashref;
}
sub write { ... }
__PACKAGE__->meta->make_immutable;
The API::User has three methods. It can create, read and write. This is all very much simplified as an example. We will focus on reading in this answer. Note how the metadata property is required, but has no default. You can't have both, because they contradict each other. You want this to be passed in, so you want it to blow up if it's missing, rather than set a default value of an empty hash reference.
Finally, in a Controller this is used as follows.
package MyApp::Controller::User;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }
__PACKAGE__->config( namespace => 'user' );
sub auto : Private {
my ( $self, $c ) = #_;
unless ( $c->session->{data}->{id} ) {
# we have to initialise data first because the model depends on it
$c->session->{data} = {};
$c->session->{data}->{id} = $c->model('API::User')->create;
}
return 1;
}
sub index_get : Path('') Args(0) GET {
my ( $self, $c ) = #_;
$c->stash->{json_data} = $c->model('API::User')->read;
return;
}
sub index_post : Path('') Args(0) POST {
my ( $self, $c ) = #_;
$c->stash->{json_data} = $c->model('API::User')->write;
return;
}
__PACKAGE__->meta->make_immutable;
I'm setting some session data in the auto action, which gets called before any other action. For a specific session this will be done once, and then that user's ID is stored in the session for subsequent requests.
In the index_get action I am accessing our class via $c->model('API::User), which will call ACCEPT_CONTEXT on our Model class, instantiate a new API::User object that is populated with both the existing database handle as well as the session metadata that contains our user's ID.
For the sake of the example, I'm using a JSON view so we can see what's happening in the DB.
When we curl the application to GET our user, the logs look as follows.
[info] *** Request 2 (0.044/s) [31642] [Fri May 6 19:01:25 2022] ***
[debug] Path is "user"
[debug] "GET" request for "user" from "127.0.0.1"
[debug] Created session "36d509c55d60c02a7a0a9cbddfae9e50b092865a"
[debug] Creating a new API::User object for MyApp::Controller::User line 15
[debug] Creating a new API::User object for MyApp::Controller::User line 23
[debug] Response Code: 200; Content-Type: application/json; charset=utf-8; Content-Length: unknown
[info] Request took 0.018616s (53.717/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /user/auto | 0.013309s |
| /user/index_get | 0.000640s |
| /end | 0.000994s |
| -> MyApp::View::JSON->process | 0.000411s |
'------------------------------------------------------------+-----------'
As you can see, we go to auto first, and then go to index_get. In the debug statements above it creates two instances of API::User. One is in auto to create a new user because I've not supplied a session cookie, and the second is from index_get.
If we call it with an existing user by supplying a session cookie (see my test script in the repository) it will only call it once.
[info] *** Request 8 (0.037/s) [31642] [Fri May 6 19:04:16 2022] ***
[debug] Path is "user"
[debug] "GET" request for "user" from "127.0.0.1"
[debug] Found sessionid "710cb37124a7042b89f1ffa650985956949df7d0" in cookie
[debug] Restored session "710cb37124a7042b89f1ffa650985956949df7d0"
[debug] Creating a new API::User object for MyApp::Controller::User line 23
[debug] Response Code: 200; Content-Type: application/json; charset=utf-8; Content-Length: unknown
[info] Request took 0.017655s (56.641/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /user/auto | 0.001887s |
| /user/index_get | 0.001238s |
| /end | 0.003510s |
| -> MyApp::View::JSON->process | 0.001463s |
'------------------------------------------------------------+-----------'
Thanks #simbabque
I have created a factory method like this
package MyApp::Model::API::Factory;
use Moose::Util;
use Module::Load qw/autoload/;
sub ACCEPT_CONTEXT {
my ( $self, $c, $args ) = #_;
my $module = 'MyApp::API::';
if(!defined $args->{api_module}) {
#eg. MyApp::Controller::API::Event::ConferenceCall::Role
my $caller_package = ( caller(2) )[ 0 ];
if($caller_package->can('api_module')) {
#get from attributes
$module .= $caller_package->new->api_module;
} else {
#auto detect/infer from caller name
$caller_package =~ /MyApp::Controller::API::(.*)/;
$module .= $1;
}
} else {
#append the prefix to the module name MyApp::API::
$module .= $args->{api_module};
}
$c->log->debug( sprintf "Creating a new %s object for %s line %d",$module,( caller(2) )[ 0, 2 ] );
my $object;
try {
autoload $module;
my $meta_method;
#auto_detect meta_method if not defined
# here check the attributes of the class and see if it has a meta_method with suffix _metadata
# if it does, use that
if(!exists $args->{meta_method}) {
my $meta = Moose::Util::find_meta($module);
my #has = $meta->get_attribute_list;
foreach my $has (#has) {
#since we have standard suffixes for the meta_methods _metadata
if($has =~ /_metadata$/ ) {
$meta_method = $has;
last;
}
}
} else {
$meta_method = $args->{meta_method};
}
$object = $module->new( $meta_method => $c->{stash}{internal});
$c->log->debug("object created by api factory for ". ref($object) . " meta attr set: $meta_method");
} catch {
$c->log->error( $_ );
return;
};
return $object;
}
1;
In every controller
my $user_api_obj = $c->model('API::Factory');
my $result = $user_api_obj->register_user($valid_params);
I have looked over this code and I can not understand the weirdness it exhibits. For a lack of understanding all I know
$c->res->redirect('qbo/home');
is being ignored, in favor of the redirect in the following if else condition. In other words, I always end up at the OAuthentication website.
If I block comment out the else condition I end up where I want to go qbo/home
sub index :Path :Args(0) {
my ($self, $c) = #_;
# Check to see if we have QBO::OAuth object in our user's session
# Create new object in session if we don't already have one
if(!($c->session->{qbo})) {
$c->log->info('Creating QBO::OAuth, save in user session');
$c->session->{qbo} = QBO::OAuth->new(
consumer_key => 'qyprddKpLkOclitN3cJCJno1fV5NzcT',
consumer_secret => 'ahwpSghVOzA142qOepNHoujyuHQFDbEzeGbZjEs3sPIc',
);
}
# Now we set our object variable to the session old or new
my $qbo = $c->session->{qbo};
######### GOTO 'qbo/home' ##########
$c->res->redirect('qbo/home');
####################################
if($c->req->params->{oauth_token}) {
$c->log->info('Now Redirect to access_endpoint');
# Get realmId and save it to our QBO::OAuth object in user session
$qbo->realmId($c->req->params->{realmId});
# Call QBO::OAuth->request_access_token
my $r = $qbo->request_access_token($c->req->params->{oauth_verifier});
$c->res->redirect('qbo/home');
} else {
my $callback = 'http://www.example.com/qbo';
# Request a token
my $r = $qbo->request_token($callback);
if($qbo->has_token) {
#Continue on down, Redirect to auth_user_endpoint
$c->res->redirect($qbo->auth_user_endpoint . '?oauth_token=' . $qbo->token);
}
}
}
Seems I am missing some basic fundamental about how this works. Any clues appreciated
From the fine manual...
This is a convenience method that sets the Location header to the redirect destination, and then sets the response status. You will want to return or $c->detach() to interrupt the normal processing flow if you want the redirect to occur straight away.
Note also the warning on that manual page about redirecting to a relative URL - you shouldn't do it. For your use-case, I'd recommend getting into the habit of using:
return $c->res->redirect($c->uri_for('qbo/home'));
or
$c->res->redirect($c->uri_for('qbo/home')) && $c->detach();
depending on your preference.
I'm trying to get away from Basic Auth in my Mojolicious application. I am able to detect the absence of a session key and redirect to a login page. The login page then posts to my application and I authenticate to a back end process. That back end process is returning success and then my mojo app sets the session like thus:
$self->session( user => $name, groups => $groups );
in debugging this, $name and $group are both defined and valid. I then wish to redirect into the "protected" space of my app. The redirect lands in the right place but then fails to detect the $self->session('user') (is undef when debugging) I end up redirecting back to login repeatedly.
I'll include snippets of the setup below. What am I missing?
MyApp.pm
my $r = $self->routes;
$r->route('/verify')->via('post')->to('util-auth#verify')->name('verify');
$r->route('/login')->via('get')->to('util-auth#login')->name('login');
my $app = $r->under('/myapp')->to('util-auth#check');
$app->route('/foo')->via('get')->to('controller-api#foo')->name('foo');
MyApp::Util::Auth
sub verify {
my $self = shift;
my $name = $self->param('username');
my $pass = $self->param('password');
my $dest = "/myapp/foo"; # in the protected area
if ( $self->authenticate($name, $pass) ) {
my $groups = $self->get_groups($name);
$self->session(
user => $name,
groups => $groups,
);
}
else {
$self->flash( message => "invalid login..." );
}
$self->redirect_to($dest);
}
sub login {
my $self = shift;
$self->render(); # renders the login form
}
sub check {
my $self = shift;
my $user = $self->session('user');
return 1 if defined $user;
$self->redirect_to('/login');
return 0;
}
I was having a similar problem and I ended up putting these in stash. I think session is string based, mainly because a cookie is set with session info.
Why your verify function accept name, pass via #_ variable?
May be need to use $self->param('name') and $self->param('pass')?
See working example here:
https://gist.github.com/Logioniz/bdf6f22c00fc51798c43
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/
I asked this over on Google's support forums for Apps integration, but got zero response. Maybe somebody here can help steer me in the right direction.
I'm trying to integrate a Perl application with Google Apps, and I'm having some trouble with the OpenID authentication. I've been using this PHP tutorial from Google as a kind of reference, since there are no Perl examples I can find.
My initial file, index.cgi (referred by manifest.xml, and the starting point of the OpenID transaction) is as follows:
use Net::OpenID::Consumer;
use CGI;
# ...
my $q = CGI->new();
my $domain = $q->param('domain');
if (!$domain) {
print $q->header(), 'Provide domain please.';
exit 0;
}
# my website
my $root = 'http://www.example.com/';
my $csr = Net::OpenID::Consumer->new(
# The user agent which sends the openid off to the server
ua => LWP::UserAgent->new,
# Who we are
required_root => $root,
# Consumer Key Secret from Google Apps Marketplace
consumer_secret => 'Zzzzzz9zzAAAAA....'
);
my $claimed_id = $csr->claimed_identity(
'https://www.google.com/accounts/o8/site-xrds?hd=' . $domain);
if ($claimed_id) {
my $check_url = $claimed_id->check_url(
# Upon validation, the user will be returned here, and real
# work may begin
return_to => $root . '/return.cgi',
trust_root => $root
);
print $q->redirect($check_url);
}
else {
print $q->header(), "Error";
}
This part seems to be working. That is, I get redirected to return.cgi with a bunch of openid.* parameters. However, at this point I get the following error:
no_identity_server The provided URL doesn't declare its OpenID identity server
I'm using the latest version of the Net::OpenID::Consumer module.
Here are the significant bits of return.cgi:
my $q = CGI->new();
my $csr = Net::OpenID::Consumer->new(
ua => LWP::UserAgent->new,
# The root of our URL
required_root => 'http://www.example.com/',
# Our password.
consumer_secret => 'Zzzzzz9zzAAAAA....',
# Where to get the information from.
args => $q
);
print $q->header();
$csr->handle_server_response(
not_openid => sub {
print "That's not an OpenID message. Did you just type in the URL?";
},
setup_required => sub {
my $setup_url = shift;
print 'You need to do something here.';
},
cancelled => sub {
print 'You cancelled your login.';
},
verified => sub {
my $vident = shift;
my $url = $vident->url;
print "You are verified as '$url'. ** FIN **";
},
error => sub { die "Can't figure it out: ", #_; }
);
As you can imagine, I'm wanting the verified sub to fire, but instead I'm getting an error. Anything obvious I'm missing? Any help would be appreciated.
So the solution, it turns out, is to switch modules. I changed to the skimpily documented Net::Google::FederatedLogin, and things are now working. The code is as follows (substitute example.com below for your actual developer's domain).
In your Google Apps Marketplace vendor profile, add the URL to index.cgi in the Application Manifest:
...
<Url>http://www.example.com/index.cgi?from=google&domain=${DOMAIN_NAME}</Url>
...
Then add the following code to your servers.
index.cgi
use CGI;
use Net::Google::FederatedLogin;
my $q = CGI->new();
my $domain = $q->param('domain');
if (!$domain) {
print $q->header(), 'Provide domain please.';
exit 0;
}
my $fl = Net::Google::FederatedLogin->new(
claimed_id =>
'https://www.google.com/accounts/o8/site-xrds?hd=' . $domain,
return_to =>
'http://www.example.com/return.cgi',
extensions => [
{
ns => 'ax',
uri => 'http://openid.net/srv/ax/1.0',
attributes => {
mode => 'fetch_request',
required => 'email',
type => {
email => 'http://axschema.org/contact/email'
}
}
}
] );
print $q->redirect($fl->get_auth_url());
return.cgi
use CGI;
use Net::Google::FederatedLogin;
my $q = CGI->new();
print $q->header();
my $fl = Net::Google::FederatedLogin->new(
cgi => $q,
return_to =>
'http://www.example.com/return.cgi' );
eval { $fl->verify_auth(); };
if ($#) {
print 'Error: ' . $#;
}
else {
# we've authenticated and gotten attributes --
my $ext = $fl->get_extension('http://openid.net/srv/ax/1.0');
print $ext->get_parameter('value.email');
}
(For a full sample, plus OAuth access to user data, see this post on my blog.)
In some cases, reportedly, this is caused by Perl missing Net::SSL, making it fail on Google's SSL URLs.
The step where it's failing is where it performs discovery on the identifier asserted by Google's server. For some reason discovery on that identifier URL is failing. It would be helpful to know what OpenID identifier the Google server is asserting to try to debug why discovery is not working for it.
I'm not sure what $domain is there, but you may need to escape it:
use URI::Escape 'uri_escape';
....
my $claimed_id = $csr->claimed_identity(
'https://www.google.com/accounts/o8/site-xrds?hd=' . uri_escape($domain) );
Also, the consumer_secret used by Net::OpenID::Consumer has no relationship to any other secret.