Commit | Line | Data |
e5b6823d |
1 | package Catalyst::Authentication::Credential::OpenID; |
d214d0c0 |
2 | use strict; |
788804c2 |
3 | # use warnings; no warnings "uninitialized"; # for testing, not production |
4 | use parent "Class::Accessor::Fast"; |
5 | |
6 | BEGIN { |
7 | __PACKAGE__->mk_accessors(qw/ _config realm debug secret /); |
8 | } |
9 | |
5f33d3e0 |
10 | our $VERSION = "0.14"; |
788804c2 |
11 | |
12 | use Net::OpenID::Consumer; |
13 | use Catalyst::Exception (); |
14 | |
15 | sub 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 | |
48 | sub 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 | |
127 | 1; |
128 | |
129 | __END__ |
130 | |
e5b6823d |
131 | =head1 NAME |
132 | |
ab944aad |
133 | Catalyst::Authentication::Credential::OpenID - OpenID credential for Catalyst::Plugin::Authentication framework. |
e5b6823d |
134 | |
d214d0c0 |
135 | =head1 VERSION |
136 | |
995b096c |
137 | 0.13 |
d214d0c0 |
138 | |
e5b6823d |
139 | =head1 SYNOPSIS |
140 | |
ab944aad |
141 | In MyApp.pm- |
d214d0c0 |
142 | |
e5b6823d |
143 | use Catalyst qw/ |
144 | Authentication |
145 | Session |
146 | Session::Store::FastMmap |
147 | Session::State::Cookie |
148 | /; |
149 | |
ab944aad |
150 | Somewhere 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 |
164 | Or 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 |
174 | In 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 |
190 | And 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 | |
199 | This is the B<third> OpenID related authentication piece for |
6342195d |
200 | L<Catalyst>. The first E<mdash> L<Catalyst::Plugin::Authentication::OpenID> |
201 | by Benjamin Trott E<mdash> was deprecated by the second E<mdash> |
e5b6823d |
202 | L<Catalyst::Plugin::Authentication::Credential::OpenID> by Tatsuhiko |
6342195d |
203 | Miyagawa E<mdash> and this is an attempt to deprecate both by conforming to |
e5b6823d |
204 | the newish, at the time of this module's inception, realm-based |
205 | authentication 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 | |
211 | The benefit of this version is that you can use an arbitrary number of |
212 | authentication systems in your L<Catalyst> application and configure |
213 | and call all of them in the same way. |
214 | |
d214d0c0 |
215 | Note that both earlier versions of OpenID authentication use the method |
e5b6823d |
216 | C<authenticate_openid()>. This module uses C<authenticate()> and |
217 | relies on you to specify the realm. You can specify the realm as the |
218 | default in the configuration or inline with each |
219 | C<authenticate()> call; more below. |
220 | |
221 | This module functions quite differently internally from the others. |
222 | See L<Catalyst::Plugin::Authentication::Internals> for more about this |
223 | implementation. |
224 | |
a404fd1a |
225 | =head1 METHODS |
e5b6823d |
226 | |
227 | =over 4 |
228 | |
d214d0c0 |
229 | =item $c->authenticate({},"your_openid_realm"); |
e5b6823d |
230 | |
231 | Call to authenticate the user via OpenID. Returns false if |
232 | authorization is unsuccessful. Sets the user into the session and |
233 | returns the user object if authentication succeeds. |
234 | |
235 | You can see in the call above that the authentication hash is empty. |
236 | The implicit OpenID parameter is, as the 2.0 specification says it |
237 | SHOULD be, B<openid_identifier>. You can set it anything you like in |
238 | your realm configuration, though, under the key C<openid_field>. If |
239 | you call C<authenticate()> with the empty info hash and no configured |
240 | C<openid_field> then only C<openid_identifier> is checked. |
241 | |
242 | It 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 | |
249 | You will never call this. Catalyst does it for you. The only important |
250 | thing you might like to know about it is that it merges its realm |
251 | configuration with its configuration proper. If this doesn't mean |
252 | anything to you, don't worry. |
e5b6823d |
253 | |
bf16184a |
254 | =back |
e5b6823d |
255 | |
256 | =head2 USER METHODS |
257 | |
258 | Currently 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 | |
282 | See L<Net::OpenID::VerifiedIdentity> for details. |
283 | |
284 | =head1 CONFIGURATION |
285 | |
286 | Catalyst authentication is now configured entirely from your |
287 | application's configuration. Do not, for example, put |
288 | C<Credential::OpenID> into your C<use Catalyst ...> statement. |
289 | Instead, tell your application that in one of your authentication |
290 | realms you will use the credential. |
291 | |
292 | In your application the following will give you two different |
293 | authentication realms. One called "members" which authenticates with |
294 | clear 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 |
340 | This 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 | |
383 | And 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 |
415 | B<NB>: There is no OpenID store yet. |
bf16184a |
416 | |
a404fd1a |
417 | =head2 EXTENSIONS TO OPENID |
418 | |
419 | The 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 | |
423 | These are set in your realm. See above. |
424 | |
425 | =over 4 |
426 | |
d214d0c0 |
427 | =item ua_args and ua_class |
bf16184a |
428 | |
6342195d |
429 | L<LWPx::ParanoidAgent> is the default agent E<mdash> C<ua_class>. You don't |
bf16184a |
430 | have to set it. I recommend that you do B<not> override it. You can |
431 | with any well behaved L<LWP::UserAgent>. You probably should not. |
e5b6823d |
432 | L<LWPx::ParanoidAgent> buys you many defenses and extra security |
433 | checks. When you allow your application users freedom to initiate |
434 | external requests, you open a big avenue for DoS (denial of service) |
435 | attacks. L<LWPx::ParanoidAgent> defends against this. |
436 | L<LWP::UserAgent> and any regular subclass of it will not. |
437 | |
d214d0c0 |
438 | =item consumer_secret |
bf16184a |
439 | |
440 | The underlying L<Net::OpenID::Consumer> object is seeded with a |
441 | secret. If it's important to you to set your own, you can. The default |
442 | uses this package name + its version + the sorted configuration keys |
443 | of your Catalyst application (chopped at 255 characters if it's |
444 | longer). This should generally be superior to any fixed string. |
445 | |
446 | =back |
447 | |
e5b6823d |
448 | =head1 TODO |
449 | |
a404fd1a |
450 | Support more of the new methods in the L<Net::OpenID> kit. |
451 | |
e5b6823d |
452 | There are some interesting implications with this sort of setup. Does |
453 | a user aggregate realms or can a user be signed in under more than one |
454 | realm? The documents could contain a recipe of the self-answering |
455 | OpenID end-point that is in the tests. |
456 | |
457 | Debug statements need to be both expanded and limited via realm |
458 | configuration. |
459 | |
bf16184a |
460 | Better diagnostics in errors. Debug info at all consumer calls. |
e5b6823d |
461 | |
bf16184a |
462 | Roles from provider domains? Mapped? Direct? A generic "openid" auto_role? |
e5b6823d |
463 | |
f29585f9 |
464 | =head1 THANKS |
465 | |
a404fd1a |
466 | To 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 |
468 | L<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 |
472 | Copyright (c) 2008, Ashley Pond V C<< <ashley@cpan.org> >>. Some of Tatsuhiko Miyagawa's work is reused here. |
e5b6823d |
473 | |
a404fd1a |
474 | This 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 | |
478 | Because this software is licensed free of charge, there is no warranty |
479 | for the software, to the extent permitted by applicable law. Except when |
480 | otherwise stated in writing the copyright holders and other parties |
481 | provide the software "as is" without warranty of any kind, either |
482 | expressed or implied, including, but not limited to, the implied |
483 | warranties of merchantability and fitness for a particular purpose. The |
484 | entire risk as to the quality and performance of the software is with |
485 | you. Should the software prove defective, you assume the cost of all |
486 | necessary servicing, repair, or correction. |
487 | |
488 | In no event unless required by applicable law or agreed to in writing |
489 | will any copyright holder, or any other party who may modify or |
490 | redistribute the software as permitted by the above license, be |
491 | liable to you for damages, including any general, special, incidental, |
492 | or consequential damages arising out of the use or inability to use |
493 | the software (including but not limited to loss of data or data being |
494 | rendered inaccurate or losses sustained by you or third parties or a |
495 | failure of the software to operate with any other software), even if |
496 | such holder or other party has been advised of the possibility of |
497 | such damages. |
498 | |
e5b6823d |
499 | =head1 SEE ALSO |
500 | |
d214d0c0 |
501 | =over 4 |
e5b6823d |
502 | |
d214d0c0 |
503 | =item OpenID |
e5b6823d |
504 | |
a404fd1a |
505 | L<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 | |
509 | L<Catalyst>, L<Catalyst::Plugin::Authentication>, L<Catalyst::Manual::Tutorial::Authorization>, and L<Catalyst::Manual::Tutorial::Authentication>. |
510 | |
1200a1ee |
511 | =item Catalyst Configuration |
d214d0c0 |
512 | |
513 | L<Catalyst::Plugin::ConfigLoader>, L<Config::General>, and L<YAML>. |
514 | |
515 | =item Miscellaneous |
516 | |
f29585f9 |
517 | L<Catalyst::Manual::Tutorial>, L<Template>, L<LWPx::ParanoidAgent>. |
d214d0c0 |
518 | |
519 | =back |
520 | |
e5b6823d |
521 | =cut |