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