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