Lost track. This is probably 0.14_04 on 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
0ad3a87d 10our $VERSION = "0.14_04";
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
41427aaf 1640.14_03
1c4d7c1f 165
06f54255 166=head1 BACKWARDS COMPATIBILITY CHANGES
167
168=head2 EXTENTION_ARGS v EXTENSIONS
1c4d7c1f 169
170B<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.
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
197 </credential>
1c4d7c1f 198 ua_class LWP::UserAgent
d214d0c0 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
1c4d7c1f 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 => {
bf16184a 356 consumer_secret => "Don't bother setting",
1c4d7c1f 357 ua_class => "LWP::UserAgent",
e5b6823d 358 ua_args => {
359 whitelisted_hosts => [qw/ 127.0.0.1 localhost /],
360 },
361 credential => {
362 class => "OpenID",
363 store => {
364 class => "OpenID",
365 },
366 },
1c4d7c1f 367 extensions => [
a404fd1a 368 'http://openid.net/extensions/sreg/1.1',
369 {
370 required => 'email',
371 optional => 'fullname,nickname,timezone',
372 },
373 ],
e5b6823d 374 },
375 },
a404fd1a 376 }
377 );
e5b6823d 378
d214d0c0 379This is the same configuration in the default L<Catalyst> configuration format from L<Config::General>.
380
381 name MyApp
382 <Plugin::Authentication>
383 default_realm members
384 <realms>
385 <members>
386 <store>
387 class Minimal
388 <users>
389 <paco>
390 password l4s4v3n7ur45
391 </paco>
392 </users>
393 </store>
394 <credential>
395 password_field password
396 password_type clear
397 class Password
398 </credential>
399 </members>
400 <openid>
401 <ua_args>
402 whitelisted_hosts 127.0.0.1
403 whitelisted_hosts localhost
404 </ua_args>
405 consumer_secret Don't bother setting
1c4d7c1f 406 ua_class LWP::UserAgent
d214d0c0 407 <credential>
408 <store>
409 class OpenID
410 </store>
411 class OpenID
412 </credential>
1c4d7c1f 413 <extensions>
a404fd1a 414 http://openid.net/extensions/sreg/1.1
415 required email
416 optional fullname,nickname,timezone
1c4d7c1f 417 </extensions>
d214d0c0 418 </openid>
419 </realms>
420 </Plugin::Authentication>
421
422And now, the same configuration in L<YAML>. B<NB>: L<YAML> is whitespace sensitive.
e5b6823d 423
424 name: MyApp
425 Plugin::Authentication:
426 default_realm: members
427 realms:
428 members:
429 credential:
430 class: Password
431 password_field: password
432 password_type: clear
433 store:
434 class: Minimal
435 users:
436 paco:
437 password: l4s4v3n7ur45
438 openid:
439 credential:
440 class: OpenID
441 store:
442 class: OpenID
bf16184a 443 consumer_secret: Don't bother setting
1c4d7c1f 444 ua_class: LWP::UserAgent
e5b6823d 445 ua_args:
446 whitelisted_hosts:
447 - 127.0.0.1
448 - localhost
1c4d7c1f 449 extensions:
a404fd1a 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
462These are set in your realm. See above.
463
464=over 4
465
d214d0c0 466=item ua_args and ua_class
bf16184a 467
06f54255 468L<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
469it. I recommend that you do B<not> override it. You can with any well behaved L<LWP::UserAgent>. You probably should not.
470L<LWPx::ParanoidAgent> buys you many defenses and extra security checks. When you allow your application users freedom to initiate external
471requests, you open an avenue for DoS (denial of service) attacks. L<LWPx::ParanoidAgent> defends against this. L<LWP::UserAgent> and any
472regular subclass of it will not.
e5b6823d 473
d214d0c0 474=item consumer_secret
bf16184a 475
06f54255 476The 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
477this package name + its version + the sorted configuration keys of your Catalyst application (chopped at 255 characters if it's longer).
478This should generally be superior to any fixed string.
bf16184a 479
480=back
481
e5b6823d 482=head1 TODO
483
1c4d7c1f 484Option to suppress fatals.
485
a404fd1a 486Support more of the new methods in the L<Net::OpenID> kit.
487
06f54255 488There 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
489realm? The documents could contain a recipe of the self-answering OpenID end-point that is in the tests.
e5b6823d 490
06f54255 491Debug statements need to be both expanded and limited via realm configuration.
e5b6823d 492
bf16184a 493Better diagnostics in errors. Debug info at all consumer calls.
e5b6823d 494
bf16184a 495Roles from provider domains? Mapped? Direct? A generic "openid" auto_role?
e5b6823d 496
f29585f9 497=head1 THANKS
498
a404fd1a 499To 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.
500
1c4d7c1f 501Menno Blom provided a bug fix and the hook to use OpenID extensions.
f29585f9 502
e5b6823d 503=head1 LICENSE AND COPYRIGHT
504
1c4d7c1f 505Copyright (c) 2008-2009, Ashley Pond V C<< <ashley@cpan.org> >>. Some of Tatsuhiko Miyagawa's work is reused here.
e5b6823d 506
a404fd1a 507This module is free software; you can redistribute it and modify it under the same terms as Perl itself. See L<perlartistic>.
e5b6823d 508
e5b6823d 509=head1 DISCLAIMER OF WARRANTY
510
06f54255 511Because this software is licensed free of charge, there is no warranty for the software, to the extent permitted by applicable law. Except
512when otherwise stated in writing the copyright holders and other parties provide the software "as is" without warranty of any kind, either
513expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The
514entire 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 515necessary servicing, repair, or correction.
516
06f54255 517In no event unless required by applicable law or agreed to in writing will any copyright holder, or any other party who may modify or
518redistribute the software as permitted by the above license, be liable to you for damages, including any general, special, incidental, or
519consequential damages arising out of the use or inability to use the software (including but not limited to loss of data or data being
520rendered inaccurate or losses sustained by you or third parties or a failure of the software to operate with any other software), even if
521such holder or other party has been advised of the possibility of such damages.
e5b6823d 522
e5b6823d 523=head1 SEE ALSO
524
d214d0c0 525=over 4
e5b6823d 526
d214d0c0 527=item OpenID
e5b6823d 528
06f54255 529L<Net::OpenID::Server>, L<Net::OpenID::VerifiedIdentity>, L<Net::OpenID::Consumer>, L<http://openid.net/>,
530L<http://openid.net/developers/specs/>, and L<http://openid.net/extensions/sreg/1.1>.
e5b6823d 531
d214d0c0 532=item Catalyst Authentication
533
06f54255 534L<Catalyst>, L<Catalyst::Plugin::Authentication>, L<Catalyst::Manual::Tutorial::Authorization>, and
535L<Catalyst::Manual::Tutorial::Authentication>.
d214d0c0 536
1200a1ee 537=item Catalyst Configuration
d214d0c0 538
539L<Catalyst::Plugin::ConfigLoader>, L<Config::General>, and L<YAML>.
540
541=item Miscellaneous
542
f29585f9 543L<Catalyst::Manual::Tutorial>, L<Template>, L<LWPx::ParanoidAgent>.
d214d0c0 544
545=back
546
e5b6823d 547=cut