File Coverage

blib/lib/Catalyst/Authentication/Credential/OpenID.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Catalyst::Authentication::Credential::OpenID;
2 1     1   22567 use warnings;
  1         2  
  1         35  
3 1     1   5 use strict;
  1         1  
  1         38  
4 1     1   155 use base "Class::Accessor::Fast";
  1         7  
  1         1082  
5              
6             __PACKAGE__->mk_accessors(qw/
7             realm debug secret
8             openid_field
9             consumer_secret
10             ua_class
11             ua_args
12             extension_args
13             errors_are_fatal
14             extensions
15             trust_root
16             flatten_extensions_into_user
17             /);
18              
19             our $VERSION = "0.19";
20              
21 1     1   5759 use Net::OpenID::Consumer;
  0            
  0            
22             use Catalyst::Exception ();
23              
24             sub new {
25             my ( $class, $config, $c, $realm ) = @_;
26             my $self = {
27             %{ $config },
28             %{ $realm->{config} }
29             };
30             bless $self, $class;
31              
32             # 2.0 spec says "SHOULD" be named "openid_identifier."
33             $self->{openid_field} ||= "openid_identifier";
34              
35             my $secret = $self->{consumer_secret} ||= join("+",
36             __PACKAGE__,
37             $VERSION,
38             sort keys %{ $c->config }
39             );
40              
41             $secret = substr($secret,0,255) if length $secret > 255;
42             $self->secret($secret);
43             # If user has no preference we prefer L::PA b/c it can prevent DoS attacks.
44             my $ua_class = $self->{ua_class} ||= eval "use LWPx::ParanoidAgent" ?
45             "LWPx::ParanoidAgent" : "LWP::UserAgent";
46              
47             my $agent_class = $self->ua_class;
48             eval "require $agent_class"
49             or Catalyst::Exception->throw("Could not 'require' user agent class " .
50             $self->ua_class);
51              
52             $c->log->debug("Setting consumer secret: " . $secret) if $self->debug;
53              
54             return $self;
55             }
56              
57             sub authenticate {
58             my ( $self, $c, $realm, $authinfo ) = @_;
59              
60             $c->log->debug("authenticate() called from " . $c->request->uri) if $self->debug;
61              
62             my $field = $self->openid_field;
63              
64             my $claimed_uri = $authinfo->{ $field };
65              
66             # Its security related so we want to be explicit about GET/POST param retrieval.
67             $claimed_uri ||= $c->req->method eq 'GET' ?
68             $c->req->query_params->{ $field } : $c->req->body_params->{ $field };
69              
70              
71             my $csr = Net::OpenID::Consumer->new(
72             ua => $self->ua_class->new(%{$self->ua_args || {}}),
73             args => $c->req->params,
74             consumer_secret => $self->secret,
75             );
76              
77             if ( $self->extension_args )
78             {
79             $c->log->warn("The configuration key 'extension_args' is ignored; use 'extensions'");
80             }
81              
82             my %extensions = ref($self->extensions) eq "HASH" ?
83             %{ $self->extensions } : ref($self->extensions) eq "ARRAY" ?
84             @{ $self->extensions } : ();
85              
86             if ( $claimed_uri )
87             {
88             my $current = $c->uri_for("/" . $c->req->path); # clear query/fragment...
89              
90             my $identity = $csr->claimed_identity($claimed_uri);
91             unless ( $identity )
92             {
93             if ( $self->errors_are_fatal )
94             {
95             Catalyst::Exception->throw($csr->err);
96             }
97             else
98             {
99             $c->log->error($csr->err . " -- $claimed_uri");
100             return;
101             }
102             }
103              
104             for my $key ( keys %extensions )
105             {
106             $identity->set_extension_args($key, $extensions{$key});
107             }
108              
109             my $check_url = $identity->check_url(
110             return_to => $current . '?openid-check=1',
111             trust_root => $self->trust_root || $current,
112             delayed_return => 1,
113             );
114             $c->res->redirect($check_url);
115             $c->detach();
116             }
117             elsif ( $c->req->params->{'openid-check'} )
118             {
119             if ( my $setup_url = $csr->user_setup_url )
120             {
121             $c->res->redirect($setup_url);
122             return;
123             }
124             elsif ( $csr->user_cancel )
125             {
126             return;
127             }
128             elsif ( my $identity = $csr->verified_identity )
129             {
130             # This is where we ought to build an OpenID user and verify against the spec.
131             my $user = +{ map { $_ => scalar $identity->$_ }
132             qw( url display rss atom foaf declared_rss declared_atom declared_foaf foafmaker ) };
133             # Dude, I did not design the array as hash spec. Don't curse me [apv].
134             for my $key ( keys %extensions )
135             {
136             my $vals = $identity->signed_extension_fields($key);
137             $user->{extensions}->{$key} = $vals;
138             if ( $self->flatten_extensions_into_user )
139             {
140             $user->{$_} = $vals->{$_} for keys %{$vals};
141             }
142             }
143              
144             my $user_obj = $realm->find_user($user, $c);
145              
146             if ( ref $user_obj )
147             {
148             return $user_obj;
149             }
150             else
151             {
152             $c->log->debug("Verified OpenID identity failed to load with find_user; bad user_class? Try 'Null.'") if $self->debug;
153             return;
154             }
155             }
156             else
157             {
158             $self->errors_are_fatal ?
159             Catalyst::Exception->throw("Error validating identity: " . $csr->err)
160             :
161             $c->log->error( $csr->err);
162             }
163             }
164             return;
165             }
166              
167             1;
168              
169             __END__
170              
171             =head1 NAME
172              
173             Catalyst::Authentication::Credential::OpenID - OpenID credential for Catalyst::Plugin::Authentication framework.
174              
175             =head1 BACKWARDS COMPATIBILITY CHANGES
176              
177             =head2 EXTENSION_ARGS v EXTENSIONS
178              
179             B<NB>: The extensions were previously configured under the key C<extension_args>. They are now configured under C<extensions>. C<extension_args> is no longer honored.
180              
181             As previously noted, L</EXTENSIONS TO OPENID>, I have not tested the extensions. I would be grateful for any feedback or, better, tests.
182              
183             =head2 FATALS
184              
185             The problems encountered by failed OpenID operations have always been fatals in the past. This is unexpected behavior for most users as it differs from other credentials. Authentication errors here are no longer fatal. Debug/error output is improved to offset the loss of information. If for some reason you would prefer the legacy/fatal behavior, set the configuration variable C<errors_are_fatal> to a true value.
186              
187             =head1 SYNOPSIS
188              
189             In MyApp.pm-
190              
191             use Catalyst qw/
192             Authentication
193             Session
194             Session::Store::FastMmap
195             Session::State::Cookie
196             /;
197              
198             Somewhere in myapp.conf-
199              
200             <Plugin::Authentication>
201             default_realm openid
202             <realms>
203             <openid>
204             <credential>
205             class OpenID
206             ua_class LWP::UserAgent
207             </credential>
208             </openid>
209             </realms>
210             </Plugin::Authentication>
211              
212             Or in your myapp.yml if you're using L<YAML> instead-
213              
214             Plugin::Authentication:
215             default_realm: openid
216             realms:
217             openid:
218             credential:
219             class: OpenID
220             ua_class: LWP::UserAgent
221              
222             In a controller, perhaps C<Root::openid>-
223              
224             sub openid : Local {
225             my($self, $c) = @_;
226              
227             if ( $c->authenticate() )
228             {
229             $c->flash(message => "You signed in with OpenID!");
230             $c->res->redirect( $c->uri_for('/') );
231             }
232             else
233             {
234             # Present OpenID form.
235             }
236             }
237              
238             And a L<Template> to match in C<openid.tt>-
239              
240             <form action="[% c.uri_for('/openid') %]" method="GET" name="openid">
241             <input type="text" name="openid_identifier" class="openid" />
242             <input type="submit" value="Sign in with OpenID" />
243             </form>
244              
245             =head1 DESCRIPTION
246              
247             This is the B<third> OpenID related authentication piece for
248             L<Catalyst>. The first E<mdash> L<Catalyst::Plugin::Authentication::OpenID>
249             by Benjamin Trott E<mdash> was deprecated by the second E<mdash>
250             L<Catalyst::Plugin::Authentication::Credential::OpenID> by Tatsuhiko
251             Miyagawa E<mdash> and this is an attempt to deprecate both by conforming to
252             the newish, at the time of this module's inception, realm-based
253             authentication in L<Catalyst::Plugin::Authentication>.
254              
255             1. Catalyst::Plugin::Authentication::OpenID
256             2. Catalyst::Plugin::Authentication::Credential::OpenID
257             3. Catalyst::Authentication::Credential::OpenID
258              
259             The benefit of this version is that you can use an arbitrary number of
260             authentication systems in your L<Catalyst> application and configure
261             and call all of them in the same way.
262              
263             Note that both earlier versions of OpenID authentication use the method
264             C<authenticate_openid()>. This module uses C<authenticate()> and
265             relies on you to specify the realm. You can specify the realm as the
266             default in the configuration or inline with each
267             C<authenticate()> call; more below.
268              
269             This module functions quite differently internally from the others.
270             See L<Catalyst::Plugin::Authentication::Internals> for more about this
271             implementation.
272              
273             =head1 METHODS
274              
275             =over 4
276              
277             =item $c->authenticate({},"your_openid_realm");
278              
279             Call to authenticate the user via OpenID. Returns false if
280             authorization is unsuccessful. Sets the user into the session and
281             returns the user object if authentication succeeds.
282              
283             You can see in the call above that the authentication hash is empty.
284             The implicit OpenID parameter is, as the 2.0 specification says it
285             SHOULD be, B<openid_identifier>. You can set it anything you like in
286             your realm configuration, though, under the key C<openid_field>. If
287             you call C<authenticate()> with the empty info hash and no configured
288             C<openid_field> then only C<openid_identifier> is checked.
289              
290             It implicitly does this (sort of, it checks the request method too)-
291              
292             my $claimed_uri = $c->req->params->{openid_identifier};
293             $c->authenticate({openid_identifier => $claimed_uri});
294              
295             =item Catalyst::Authentication::Credential::OpenID->new()
296              
297             You will never call this. Catalyst does it for you. The only important
298             thing you might like to know about it is that it merges its realm
299             configuration with its configuration proper. If this doesn't mean
300             anything to you, don't worry.
301              
302             =back
303              
304             =head2 USER METHODS
305              
306             Currently the only supported user class is L<Catalyst::Plugin::Authentication::User::Hash>.
307              
308             =over 4
309              
310             =item $c->user->url
311              
312             =item $c->user->display
313              
314             =item $c->user->rss
315              
316             =item $c->user->atom
317              
318             =item $c->user->foaf
319              
320             =item $c->user->declared_rss
321              
322             =item $c->user->declared_atom
323              
324             =item $c->user->declared_foaf
325              
326             =item $c->user->foafmaker
327              
328             =back
329              
330             See L<Net::OpenID::VerifiedIdentity> for details.
331              
332             =head1 CONFIGURATION
333              
334             Catalyst authentication is now configured entirely from your
335             application's configuration. Do not, for example, put
336             C<Credential::OpenID> into your C<use Catalyst ...> statement.
337             Instead, tell your application that in one of your authentication
338             realms you will use the credential.
339              
340             In your application the following will give you two different
341             authentication realms. One called "members" which authenticates with
342             clear text passwords and one called "openid" which uses... uh, OpenID.
343              
344             __PACKAGE__->config
345             ( name => "MyApp",
346             "Plugin::Authentication" => {
347             default_realm => "members",
348             realms => {
349             members => {
350             credential => {
351             class => "Password",
352             password_field => "password",
353             password_type => "clear"
354             },
355             store => {
356             class => "Minimal",
357             users => {
358             paco => {
359             password => "l4s4v3n7ur45",
360             },
361             }
362             }
363             },
364             openid => {
365             credential => {
366             class => "OpenID",
367             store => {
368             class => "OpenID",
369             },
370             consumer_secret => "Don't bother setting",
371             ua_class => "LWP::UserAgent",
372             # whitelist is only relevant for LWPx::ParanoidAgent
373             ua_args => {
374             whitelisted_hosts => [qw/ 127.0.0.1 localhost /],
375             },
376             extensions => [
377             'http://openid.net/extensions/sreg/1.1',
378             {
379             required => 'email',
380             optional => 'fullname,nickname,timezone',
381             },
382             ],
383             },
384             },
385             },
386             }
387             );
388              
389             This is the same configuration in the default L<Catalyst> configuration format from L<Config::General>.
390              
391             name MyApp
392             <Plugin::Authentication>
393             default_realm members
394             <realms>
395             <members>
396             <store>
397             class Minimal
398             <users>
399             <paco>
400             password l4s4v3n7ur45
401             </paco>
402             </users>
403             </store>
404             <credential>
405             password_field password
406             password_type clear
407             class Password
408             </credential>
409             </members>
410             <openid>
411             <credential>
412             <store>
413             class OpenID
414             </store>
415             class OpenID
416             <ua_args>
417             whitelisted_hosts 127.0.0.1
418             whitelisted_hosts localhost
419             </ua_args>
420             consumer_secret Don't bother setting
421             ua_class LWP::UserAgent
422             <extensions>
423             http://openid.net/extensions/sreg/1.1
424             required email
425             optional fullname,nickname,timezone
426             </extensions>
427             </credential>
428             </openid>
429             </realms>
430             </Plugin::Authentication>
431              
432             And now, the same configuration in L<YAML>. B<NB>: L<YAML> is whitespace sensitive.
433              
434             name: MyApp
435             Plugin::Authentication:
436             default_realm: members
437             realms:
438             members:
439             credential:
440             class: Password
441             password_field: password
442             password_type: clear
443             store:
444             class: Minimal
445             users:
446             paco:
447             password: l4s4v3n7ur45
448             openid:
449             credential:
450             class: OpenID
451             store:
452             class: OpenID
453             consumer_secret: Don't bother setting
454             ua_class: LWP::UserAgent
455             ua_args:
456             # whitelist is only relevant for LWPx::ParanoidAgent
457             whitelisted_hosts:
458             - 127.0.0.1
459             - localhost
460             extensions:
461             - http://openid.net/extensions/sreg/1.1
462             - required: email
463             optional: fullname,nickname,timezone
464              
465             B<NB>: There is no OpenID store yet.
466              
467             You can set C<trust_root> now too. This is experimental and I have no idea if it's right or could be better. Right now it must be a URI. It was submitted as a path but this seems to limit it to the Catalyst app and while easier to dynamically generate no matter where the app starts, it seems like the wrong way to go. Let me know if that's mistaken.
468              
469             =head2 EXTENSIONS TO OPENID
470              
471             The Simple Registration--L<http://openid.net/extensions/sreg/1.1>--(SREG) extension to OpenID is supported in the L<Net::OpenID> family now. Experimental support for it is included here as of v0.12. SREG is the only supported extension in OpenID 1.1. It's experimental in the sense it's a new interface and barely tested. Support for OpenID extensions is here to stay.
472              
473             Google's OpenID is also now supported. Uh, I think.
474              
475             Here is a snippet from Thorben JE<auml>ndling combining Sreg and Google's extenstionsE<ndash>
476              
477             'Plugin::Authentication' => {
478             openid => {
479             credential => {
480             class => 'OpenID',
481             ua_class => 'LWP::UserAgent',
482             extensions => {
483             'http://openid.net/extensions/sreg/1.1' => {
484             required => 'nickname,email,fullname',
485             optional => 'timezone,language,dob,country,gender'
486             },
487             'http://openid.net/srv/ax/1.0' => {
488             mode => 'fetch_request',
489             'type.nickname' => 'http://axschema.org/namePerson/friendly',
490             'type.email' => 'http://axschema.org/contact/email',
491             'type.fullname' => 'http://axschema.org/namePerson',
492             'type.firstname' => 'http://axschema.org/namePerson/first',
493             'type.lastname' => 'http://axschema.org/namePerson/last',
494             'type.dob' => 'http://axschema.org/birthDate',
495             'type.gender' => 'http://axschema.org/person/gender',
496             'type.country' => 'http://axschema.org/contact/country/home',
497             'type.language' => 'http://axschema.org/pref/language',
498             'type.timezone' => 'http://axschema.org/pref/timezone',
499             required => 'nickname,fullname,email,firstname,lastname',
500             if_available => 'dob,gender,country,language,timezone',
501             },
502             },
503             },
504             },
505             default_realm => 'openid',
506             };
507              
508              
509             =head2 MORE ON CONFIGURATION
510              
511             =over 4
512              
513             =item ua_args and ua_class
514              
515             L<LWPx::ParanoidAgent> is the default agent E<mdash> C<ua_class> E<mdash> if it's available, L<LWP::UserAgent> if not. You don't have to set it. I recommend that you do B<not> override it. You can with any well behaved L<LWP::UserAgent>. You probably should not. L<LWPx::ParanoidAgent> buys you many defenses and extra security checks. When you allow your application users freedom to initiate external requests, you open an avenue for DoS (denial of service) attacks. L<LWPx::ParanoidAgent> defends against this. L<LWP::UserAgent> and any regular subclass of it will not.
516              
517             =item consumer_secret
518              
519             The underlying L<Net::OpenID::Consumer> object is seeded with a secret. If it's important to you to set your own, you can. The default uses this package name + its version + the sorted configuration keys of your Catalyst application (chopped at 255 characters if it's longer). This should generally be superior to any fixed string.
520              
521             =back
522              
523             =head1 TODO
524              
525             Option to suppress fatals.
526              
527             Support more of the new methods in the L<Net::OpenID> kit.
528              
529             There are some interesting implications with this sort of setup. Does a user aggregate realms or can a user be signed in under more than one realm? The documents could contain a recipe of the self-answering OpenID end-point that is in the tests.
530              
531             Debug statements need to be both expanded and limited via realm configuration.
532              
533             Better diagnostics in errors. Debug info at all consumer calls.
534              
535             Roles from provider domains? Mapped? Direct? A generic "openid" auto_role?
536              
537             =head1 THANKS
538              
539             To Benjamin Trott (L<Catalyst::Plugin::Authentication::OpenID>), Tatsuhiko Miyagawa (L<Catalyst::Plugin::Authentication::Credential::OpenID>), Brad Fitzpatrick for the great OpenID stuff, Martin Atkins for picking up the code to handle OpenID 2.0, and Jay Kuri and everyone else who has made Catalyst such a wonderful framework.
540              
541             Menno Blom provided a bug fix and the hook to use OpenID extensions.
542              
543             =head1 LICENSE AND COPYRIGHT
544              
545             Copyright (c) 2008-2009, Ashley Pond V C<< <ashley@cpan.org> >>. Some of Tatsuhiko Miyagawa's work is reused here.
546              
547             This module is free software; you can redistribute it and modify it under the same terms as Perl itself. See L<perlartistic>.
548              
549             =head1 DISCLAIMER OF WARRANTY
550              
551             Because this software is licensed free of charge, there is no warranty for the software, to the extent permitted by applicable law. Except
552             when otherwise stated in writing the copyright holders and other parties provide the software "as is" without warranty of any kind, either
553             expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The
554             entire risk as to the quality and performance of the software is with you. Should the software prove defective, you assume the cost of all
555             necessary servicing, repair, or correction.
556              
557             In no event unless required by applicable law or agreed to in writing will any copyright holder, or any other party who may modify or
558             redistribute the software as permitted by the above license, be liable to you for damages, including any general, special, incidental, or
559             consequential damages arising out of the use or inability to use the software (including but not limited to loss of data or data being
560             rendered inaccurate or losses sustained by you or third parties or a failure of the software to operate with any other software), even if
561             such holder or other party has been advised of the possibility of such damages.
562              
563             =head1 SEE ALSO
564              
565             =over 4
566              
567             =item OpenID
568              
569             L<Net::OpenID::Server>, L<Net::OpenID::VerifiedIdentity>, L<Net::OpenID::Consumer>, L<http://openid.net/>,
570             L<http://openid.net/developers/specs/>, and L<http://openid.net/extensions/sreg/1.1>.
571              
572             =item Catalyst Authentication
573              
574             L<Catalyst>, L<Catalyst::Plugin::Authentication>, L<Catalyst::Manual::Tutorial::Authorization>, and
575             L<Catalyst::Manual::Tutorial::Authentication>.
576              
577             =item Catalyst Configuration
578              
579             L<Catalyst::Plugin::ConfigLoader>, L<Config::General>, and L<YAML>.
580              
581             =item Miscellaneous
582              
583             L<Catalyst::Manual::Tutorial>, L<Template>, L<LWPx::ParanoidAgent>.
584              
585             =back
586              
587             =cut