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