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