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