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