This is 0.15 on the way up to the CPAN.
[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
29b37787 10our $VERSION = "0.15";
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
06f54255 63
e5b6823d 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
06f54255 70 if ( $self->_config->{extension_args} and $self->debug )
71 {
72 $c->log->info("The configuration key 'extension_args' is deprecated; use 'extensions'");
73 }
74
1c4d7c1f 75 my @extensions = $self->_config->{extensions} ?
76 @{ $self->_config->{extensions} } : $self->_config->{extension_args} ?
77 @{ $self->_config->{extension_args} } : ();
78
e5b6823d 79 if ( $claimed_uri )
80 {
81 my $current = $c->uri_for($c->req->uri->path); # clear query/fragment...
82
41427aaf 83 my $identity = $csr->claimed_identity($claimed_uri);
84 unless ( $identity )
85 {
06f54255 86 if ( $self->_config->{errors_are_fatal} )
87 {
88 Catalyst::Exception->throw($csr->err);
89 }
90 else
91 {
92 $c->log->error($csr->err . " -- $claimed_uri");
93 $c->detach();
94 }
41427aaf 95 }
e5b6823d 96
5bd16806 97 $identity->set_extension_args(@extensions)
1c4d7c1f 98 if @extensions;
a404fd1a 99
e5b6823d 100 my $check_url = $identity->check_url(
101 return_to => $current . '?openid-check=1',
102 trust_root => $current,
103 delayed_return => 1,
104 );
105 $c->res->redirect($check_url);
85db8ed7 106 $c->detach();
e5b6823d 107 }
108 elsif ( $c->req->params->{'openid-check'} )
109 {
110 if ( my $setup_url = $csr->user_setup_url )
111 {
112 $c->res->redirect($setup_url);
113 return;
114 }
115 elsif ( $csr->user_cancel )
116 {
117 return;
118 }
119 elsif ( my $identity = $csr->verified_identity )
120 {
121 # This is where we ought to build an OpenID user and verify against the spec.
122 my $user = +{ map { $_ => scalar $identity->$_ }
123 qw( url display rss atom foaf declared_rss declared_atom declared_foaf foafmaker ) };
1c4d7c1f 124 # Dude, I did not design the array as hash spec. Don't curse me [apv].
125 my %flat = @extensions;
126 for my $key ( keys %flat )
127 {
128 $user->{extensions}->{$key} = $identity->signed_extension_fields($key);
a404fd1a 129 }
e5b6823d 130
131 my $user_obj = $realm->find_user($user, $c);
132
133 if ( ref $user_obj )
134 {
135 return $user_obj;
136 }
137 else
138 {
06f54255 139 $c->log->debug("Verified OpenID identity failed to load with find_user; bad user_class? Try 'Null.'") if $self->debug;
e5b6823d 140 return;
141 }
142 }
143 else
144 {
06f54255 145 $self->_config->{errors_are_fatal} ?
146 Catalyst::Exception->throw("Error validating identity: " . $csr->err)
147 :
148 $c->log->error( $csr->err);
e5b6823d 149 }
150 }
d214d0c0 151 return;
e5b6823d 152}
153
1541;
155
156__END__
157
e5b6823d 158=head1 NAME
159
ab944aad 160Catalyst::Authentication::Credential::OpenID - OpenID credential for Catalyst::Plugin::Authentication framework.
e5b6823d 161
d214d0c0 162=head1 VERSION
163
29b37787 1640.15
1c4d7c1f 165
06f54255 166=head1 BACKWARDS COMPATIBILITY CHANGES
167
29b37787 168=head2 EXTENSION_ARGS v EXTENSIONS
1c4d7c1f 169
29b37787 170B<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 171
172As previously noted, L</EXTENSIONS TO OPENID>, I have not tested the extensions. I would be grateful for any feedback or, better, tests.
d214d0c0 173
06f54255 174=head2 FATALS
175
176The 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.
177
e5b6823d 178=head1 SYNOPSIS
179
ab944aad 180In MyApp.pm-
d214d0c0 181
e5b6823d 182 use Catalyst qw/
183 Authentication
184 Session
185 Session::Store::FastMmap
186 Session::State::Cookie
187 /;
188
ab944aad 189Somewhere in myapp.conf-
d214d0c0 190
191 <Plugin::Authentication>
192 default_realm openid
193 <realms>
194 <openid>
d214d0c0 195 <credential>
d214d0c0 196 class OpenID
29b37787 197 ua_class LWP::UserAgent
d214d0c0 198 </credential>
199 </openid>
200 </realms>
201 </Plugin::Authentication>
202
ab944aad 203Or in your myapp.yml if you're using L<YAML> instead-
d214d0c0 204
e5b6823d 205 Plugin::Authentication:
206 default_realm: openid
207 realms:
208 openid:
209 credential:
210 class: OpenID
29b37787 211 ua_class: LWP::UserAgent
d214d0c0 212
ab944aad 213In a controller, perhaps C<Root::openid>-
e5b6823d 214
bf16184a 215 sub openid : Local {
e5b6823d 216 my($self, $c) = @_;
217
218 if ( $c->authenticate() )
219 {
220 $c->flash(message => "You signed in with OpenID!");
221 $c->res->redirect( $c->uri_for('/') );
222 }
223 else
224 {
225 # Present OpenID form.
226 }
bf16184a 227 }
e5b6823d 228
ab944aad 229And a L<Template> to match in C<openid.tt>-
d214d0c0 230
bf16184a 231 <form action="[% c.uri_for('/openid') %]" method="GET" name="openid">
232 <input type="text" name="openid_identifier" class="openid" />
233 <input type="submit" value="Sign in with OpenID" />
234 </form>
e5b6823d 235
e5b6823d 236=head1 DESCRIPTION
237
238This is the B<third> OpenID related authentication piece for
6342195d 239L<Catalyst>. The first E<mdash> L<Catalyst::Plugin::Authentication::OpenID>
240by Benjamin Trott E<mdash> was deprecated by the second E<mdash>
e5b6823d 241L<Catalyst::Plugin::Authentication::Credential::OpenID> by Tatsuhiko
6342195d 242Miyagawa E<mdash> and this is an attempt to deprecate both by conforming to
e5b6823d 243the newish, at the time of this module's inception, realm-based
244authentication in L<Catalyst::Plugin::Authentication>.
245
d214d0c0 246 1. Catalyst::Plugin::Authentication::OpenID
247 2. Catalyst::Plugin::Authentication::Credential::OpenID
248 3. Catalyst::Authentication::Credential::OpenID
e5b6823d 249
250The benefit of this version is that you can use an arbitrary number of
251authentication systems in your L<Catalyst> application and configure
252and call all of them in the same way.
253
d214d0c0 254Note that both earlier versions of OpenID authentication use the method
e5b6823d 255C<authenticate_openid()>. This module uses C<authenticate()> and
256relies on you to specify the realm. You can specify the realm as the
257default in the configuration or inline with each
258C<authenticate()> call; more below.
259
260This module functions quite differently internally from the others.
261See L<Catalyst::Plugin::Authentication::Internals> for more about this
262implementation.
263
a404fd1a 264=head1 METHODS
e5b6823d 265
266=over 4
267
d214d0c0 268=item $c->authenticate({},"your_openid_realm");
e5b6823d 269
270Call to authenticate the user via OpenID. Returns false if
271authorization is unsuccessful. Sets the user into the session and
272returns the user object if authentication succeeds.
273
274You can see in the call above that the authentication hash is empty.
275The implicit OpenID parameter is, as the 2.0 specification says it
276SHOULD be, B<openid_identifier>. You can set it anything you like in
277your realm configuration, though, under the key C<openid_field>. If
278you call C<authenticate()> with the empty info hash and no configured
279C<openid_field> then only C<openid_identifier> is checked.
280
281It implicitly does this (sort of, it checks the request method too)-
282
283 my $claimed_uri = $c->req->params->{openid_identifier};
284 $c->authenticate({openid_identifier => $claimed_uri});
285
d214d0c0 286=item Catalyst::Authentication::Credential::OpenID->new()
bf16184a 287
288You will never call this. Catalyst does it for you. The only important
289thing you might like to know about it is that it merges its realm
290configuration with its configuration proper. If this doesn't mean
291anything to you, don't worry.
e5b6823d 292
bf16184a 293=back
e5b6823d 294
295=head2 USER METHODS
296
297Currently the only supported user class is L<Catalyst::Plugin::Authentication::User::Hash>.
298
bf16184a 299=over 4
e5b6823d 300
d214d0c0 301=item $c->user->url
e5b6823d 302
d214d0c0 303=item $c->user->display
e5b6823d 304
d214d0c0 305=item $c->user->rss
e5b6823d 306
d214d0c0 307=item $c->user->atom
e5b6823d 308
d214d0c0 309=item $c->user->foaf
e5b6823d 310
d214d0c0 311=item $c->user->declared_rss
e5b6823d 312
d214d0c0 313=item $c->user->declared_atom
e5b6823d 314
d214d0c0 315=item $c->user->declared_foaf
e5b6823d 316
d214d0c0 317=item $c->user->foafmaker
e5b6823d 318
319=back
320
321See L<Net::OpenID::VerifiedIdentity> for details.
322
323=head1 CONFIGURATION
324
325Catalyst authentication is now configured entirely from your
326application's configuration. Do not, for example, put
327C<Credential::OpenID> into your C<use Catalyst ...> statement.
328Instead, tell your application that in one of your authentication
329realms you will use the credential.
330
331In your application the following will give you two different
332authentication realms. One called "members" which authenticates with
333clear text passwords and one called "openid" which uses... uh, OpenID.
334
335 __PACKAGE__->config
336 ( name => "MyApp",
337 "Plugin::Authentication" => {
338 default_realm => "members",
339 realms => {
340 members => {
341 credential => {
342 class => "Password",
343 password_field => "password",
344 password_type => "clear"
345 },
346 store => {
347 class => "Minimal",
348 users => {
349 paco => {
350 password => "l4s4v3n7ur45",
351 },
352 }
353 }
354 },
355 openid => {
e5b6823d 356 credential => {
357 class => "OpenID",
358 store => {
359 class => "OpenID",
360 },
29b37787 361 consumer_secret => "Don't bother setting",
362 ua_class => "LWP::UserAgent",
363 # whitelist is only relevant for LWPx::ParanoidAgent
364 ua_args => {
365 whitelisted_hosts => [qw/ 127.0.0.1 localhost /],
a404fd1a 366 },
29b37787 367 extensions => [
368 'http://openid.net/extensions/sreg/1.1',
369 {
370 required => 'email',
371 optional => 'fullname,nickname,timezone',
372 },
373 ],
374 },
e5b6823d 375 },
376 },
a404fd1a 377 }
378 );
e5b6823d 379
d214d0c0 380This is the same configuration in the default L<Catalyst> configuration format from L<Config::General>.
381
382 name MyApp
383 <Plugin::Authentication>
384 default_realm members
385 <realms>
386 <members>
387 <store>
388 class Minimal
389 <users>
390 <paco>
391 password l4s4v3n7ur45
392 </paco>
393 </users>
394 </store>
395 <credential>
396 password_field password
397 password_type clear
398 class Password
399 </credential>
400 </members>
401 <openid>
d214d0c0 402 <credential>
403 <store>
404 class OpenID
405 </store>
406 class OpenID
29b37787 407 <ua_args>
408 whitelisted_hosts 127.0.0.1
409 whitelisted_hosts localhost
410 </ua_args>
411 consumer_secret Don't bother setting
412 ua_class LWP::UserAgent
413 <extensions>
414 http://openid.net/extensions/sreg/1.1
415 required email
416 optional fullname,nickname,timezone
417 </extensions>
d214d0c0 418 </credential>
419 </openid>
420 </realms>
421 </Plugin::Authentication>
422
423And now, the same configuration in L<YAML>. B<NB>: L<YAML> is whitespace sensitive.
e5b6823d 424
425 name: MyApp
426 Plugin::Authentication:
427 default_realm: members
428 realms:
429 members:
430 credential:
431 class: Password
432 password_field: password
433 password_type: clear
434 store:
435 class: Minimal
436 users:
437 paco:
438 password: l4s4v3n7ur45
439 openid:
440 credential:
441 class: OpenID
442 store:
443 class: OpenID
29b37787 444 consumer_secret: Don't bother setting
445 ua_class: LWP::UserAgent
446 ua_args:
447 # whitelist is only relevant for LWPx::ParanoidAgent
448 whitelisted_hosts:
449 - 127.0.0.1
450 - localhost
451 extensions:
452 - http://openid.net/extensions/sreg/1.1
453 - required: email
454 optional: fullname,nickname,timezone
e5b6823d 455
6342195d 456B<NB>: There is no OpenID store yet.
bf16184a 457
a404fd1a 458=head2 EXTENSIONS TO OPENID
459
1c4d7c1f 460The 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 461
d214d0c0 462=head2 MORE ON CONFIGURATION
bf16184a 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