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