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