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