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