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