It's ->find_user($authinfo, $c), not ->find_user($c, $authinfo).
[catagits/Catalyst-Authentication-Realm-Adaptor.git] / lib / Catalyst / Authentication / Realm / Adaptor.pm
CommitLineData
c6a2d572 1package Catalyst::Authentication::Realm::Adaptor;
2
3use warnings;
4use strict;
5use Carp;
6use Moose;
7extends 'Catalyst::Authentication::Realm';
8
9=head1 NAME
10
11Catalyst::Authentication::Realm::Adaptor - Adjust parameters of authentication processes on the fly
12
13=head1 VERSION
14
15Version 0.01
16
17=cut
18
19## goes in catagits@jules.scsys.co.uk:Catalyst-Authentication-Realm-Adaptor.git
20
21our $VERSION = '0.01';
22
23sub authenticate {
24 my ( $self, $c, $authinfo ) = @_;
25
26 my $newauthinfo;
924b3b67 27
c6a2d572 28 if (exists($self->config->{'credential_adaptor'})) {
924b3b67 29
c6a2d572 30 if ($self->config->{'credential_adaptor'}{'method'} eq 'merge_hash') {
924b3b67 31
c6a2d572 32 $newauthinfo = _munge_hash($authinfo, $self->config->{'credential_adaptor'}{'merge_hash'}, $authinfo);
924b3b67 33
c6a2d572 34 } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'new_hash') {
924b3b67 35
c6a2d572 36 $newauthinfo = _munge_hash({}, $self->config->{'credential_adaptor'}{'new_hash'}, $authinfo);
924b3b67 37
c6a2d572 38 } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'action') {
924b3b67 39
c6a2d572 40 my $controller = $c->controller($self->config->{'credential_adaptor'}{'controller'});
41 if (!$controller) {
924b3b67 42 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use a controller that doesn't exist: " .
c6a2d572 43 $self->config->{'credential_adaptor'}{'controller'});
924b3b67 44 }
45
c6a2d572 46 my $action = $controller->action_for($self->config->{'credential_adaptor'}{'action'});
47 if (!$action) {
924b3b67 48 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use an action that doesn't exist: " .
c6a2d572 49 $self->config->{'credential_adaptor'}{'controller'} . "->" .
50 $self->config->{'credential_adaptor'}{'action'});
51 }
52 $newauthinfo = $c->forward($action, $self->name, $authinfo, $self->config->{'credential_adaptor'});
924b3b67 53
c6a2d572 54 } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'code' ) {
924b3b67 55
c6a2d572 56 if (ref($self->config->{'credential_adaptor'}{'code'}) eq 'CODE') {
57 my $sub = $self->config->{'credential_adaptor'}{'code'};
58 $newauthinfo = $sub->($self->name, $authinfo, $self->config->{'credential_adaptor'});
59 } else {
60 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor is configured to use a code ref that doesn't exist");
61 }
62 }
63 return $self->SUPER::authenticate($c, $newauthinfo);
64 } else {
65 return $self->SUPER::authenticate($c, $authinfo);
66 }
67}
68
69sub find_user {
70 my ( $self, $authinfo, $c ) = @_;
71
72 my $newauthinfo;
924b3b67 73
c6a2d572 74 if (exists($self->config->{'store_adaptor'})) {
924b3b67 75
c6a2d572 76 if ($self->config->{'store_adaptor'}{'method'} eq 'merge_hash') {
924b3b67 77
c6a2d572 78 $newauthinfo = _munge_hash($authinfo, $self->config->{'store_adaptor'}{'merge_hash'}, $authinfo);
924b3b67 79
c6a2d572 80 } elsif ($self->config->{'store_adaptor'}{'method'} eq 'new_hash') {
924b3b67 81
c6a2d572 82 $newauthinfo = _munge_hash({}, $self->config->{'store_adaptor'}{'new_hash'}, $authinfo);
924b3b67 83
c6a2d572 84 } elsif ($self->config->{'store_adaptor'}{'method'} eq 'action') {
924b3b67 85
c6a2d572 86 my $controller = $c->controller($self->config->{'store_adaptor'}{'controller'});
87 if (!$controller) {
924b3b67 88 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use a controller that doesn't exist: " .
c6a2d572 89 $self->config->{'store_adaptor'}{'controller'});
924b3b67 90 }
91
c6a2d572 92 my $action = $controller->action_for($self->config->{'store_adaptor'}{'action'});
93 if (!$action) {
924b3b67 94 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use an action that doesn't exist: " .
c6a2d572 95 $self->config->{'store_adaptor'}{'controller'} . "->" .
96 $self->config->{'store_adaptor'}{'action'});
97 }
98 $newauthinfo = $c->forward($action, $self->name, $authinfo, $self->config->{'store_adaptor'});
924b3b67 99
c6a2d572 100 } elsif ($self->config->{'store_adaptor'}{'method'} eq 'code' ) {
924b3b67 101
c6a2d572 102 if (ref($self->config->{'store_adaptor'}{'code'}) eq 'CODE') {
103 my $sub = $self->config->{'store_adaptor'}{'code'};
104 $newauthinfo = $sub->($self->name, $authinfo, $self->config->{'store_adaptor'});
105 } else {
106 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor is configured to use a code ref that doesn't exist");
107 }
108 }
b6ee801b 109 return $self->SUPER::find_user($newauthinfo, $c);
c6a2d572 110 } else {
b6ee801b 111 return $self->SUPER::find_user($authinfo, $c);
c6a2d572 112 }
113}
114
115sub _munge_hash {
116 my ($sourcehash, $modhash, $referencehash) = @_;
924b3b67 117
c6a2d572 118 my $resulthash = { %{$sourcehash} };
924b3b67 119
c6a2d572 120 foreach my $key (keys %{$modhash}) {
121 if (ref($modhash->{$key}) eq 'HASH') {
122 if (ref($sourcehash->{$key}) eq 'HASH') {
123 $resulthash->{$key} = _munge_hash($sourcehash->{$key}, $modhash->{$key}, $referencehash)
124 } else {
125 $resulthash->{$key} = _munge_hash({}, $modhash->{$key}, $referencehash);
126 }
127 } else {
128 if (ref($modhash->{$key} eq 'ARRAY') && ref($sourcehash->{$key}) eq 'ARRAY') {
129 push @{$resulthash->{$key}}, _munge_value($modhash->{$key}, $referencehash)
130 }
924b3b67 131 $resulthash->{$key} = _munge_value($modhash->{$key}, $referencehash);
c6a2d572 132 if (ref($resulthash->{$key}) eq 'SCALAR' && ${$resulthash->{$key}} eq '-') {
133 ## Scalar reference to a string '-' means delete the element from the source array.
134 delete($resulthash->{$key});
135 }
136 }
137 }
138 return($resulthash);
139}
140
141sub _munge_value {
142 my ($modvalue, $referencehash) = @_;
924b3b67 143
c6a2d572 144 my $newvalue;
145 if ($modvalue =~ m/^([+-])\((.*)\)$/) {
146 my $action = $1;
147 my $keypath = $2;
924b3b67 148 ## do magic
c6a2d572 149 if ($action eq '+') {
150 ## action = string '-' means delete the element from the source array.
151 ## otherwise it means copy it from a field in the original hash with nesting
152 ## indicated via '.' - IE similar to Template Toolkit handling of nested hashes
153 my @hashpath = split /\./, $keypath;
154 my $val = $referencehash;
155 foreach my $subkey (@hashpath) {
156 if (ref($val) eq 'HASH') {
157 $val = $val->{$subkey};
158 } elsif (ref($val) eq 'ARRAY') {
159 $val = $val->[$subkey];
160 } else {
161 ## failed to find that key in the hash / array
162 $val = undef;
163 last;
164 }
165 }
166 $newvalue = $val;
167 } else {
168 ## delete the value... so we return a scalar ref to '-'
169 $newvalue = \'-';
170 }
171 } elsif (ref($modvalue) eq 'ARRAY') {
172 $newvalue = [];
173 foreach my $row (0..$#{$modvalue}) {
174 if (defined($modvalue->[$row])) {
175 my $val = _munge_value($modvalue->[$row], $referencehash);
176 ## this is the first time I've ever wanted to use unless
177 ## to make things clearer
178 unless (ref($val) eq 'SCALAR' && ${$val} eq '-') {
924b3b67 179 $newvalue->[$row] = $val;
c6a2d572 180 }
181 }
182 }
183 } else {
184 $newvalue = $modvalue;
185 }
186 return $newvalue;
187}
188
189
190=head1 SYNOPSIS
191
192The Catalyst::Authentication::Realm::Adaptor allows for modification of
193authentication parameters within the catalyst application. It's basically a
194filter used to adjust authentication parameters globally within the
195application or to adjust user retrieval parameters provided by the credential
196in order to be compatible with a different store. It provides for better
197control over interaction between credentials and stores. This is particularly
198useful when working with external authentication such as OpenID or OAuth.
199
200 __PACKAGE__->config(
201 'Plugin::Authentication' => {
202 'default' => {
203 class => 'Adaptor'
204 credential => {
205 class => 'Password',
206 password_field => 'secret',
207 password_type => 'hashed',
208 password_hash_type => 'SHA-1',
209 },
210 store => {
211 class => 'DBIx::Class',
212 user_class => 'Schema::Person',
213 },
214 store_adaptor => {
215 method => 'merge_hash',
216 merge_hash => {
217 status => [ 'temporary', 'active' ]
218 }
219 }
220 },
221 }
222 }
223 );
224
225
226The above example ensures that no matter how $c->authenticate() is called
227within your application, the key 'status' is added to the authentication hash.
228This allows you to, among other things, set parameters that should always be
229applied to your authentication process or modify the parameters to better
230connect a credential and a store that were not built to work together. In the
231above example, we are making sure that the user search is restricted to those
232with a status of either 'temporary' or 'active.'
233
234This realm works by intercepting the original authentication information
235between the time C<< $c->authenticate($authinfo) >> is called and the time the
d0102990 236realm's C<< $realm->authenticate($c,$authinfo) >> method is called, allowing for
c6a2d572 237the $authinfo parameter to be modified or replaced as your application
238requires. It can also operate after the call to the credential's
239C<authenticate()> method but before the call to the store's C<find_user>
240method.
241
242If you don't know what the above means, you probably do not need this module.
243
244=head1 CONFIGURATION
245
246The configuration for this module goes within your realm configuration alongside your
924b3b67 247credential and store options.
c6a2d572 248
924b3b67 249This module can operate in two points during authentication processing.
c6a2d572 250The first is prior the realm's C<authenticate> call (immediately after the call to
d0102990 251C<< $c->authenticate() >>.) To operate here, your filter options should go in a hash
c6a2d572 252under the key C<credential_adaptor>.
253
254The second point is after the call to credential's C<authenticate> method but
924b3b67 255immediately before the call to the user store's C<find_user> method. To operate
256prior to C<find_user>, your filter options should go in a hash under the key
c6a2d572 257C<store_adaptor>.
258
259The filtering options for both points are the same, and both the C<store_adaptor> and
260C<credential_adaptor> can be used simultaneously in a single realm.
261
262=head2 method
263
924b3b67 264There are four ways to configure your filters. You specify which one you want by setting
c6a2d572 265the C<method> configuration option to one of the following: C<merge_hash>, C<new_hash>,
266C<code>, or C<action>. You then provide the additional information based on which method
267you have chosen. The different options are described below.
268
269=over 8
270
271=item merge_hash
272
273 credential_adaptor => {
274 method => 'merge_hash',
275 merge_hash => {
276 status => [ 'temporary', 'active' ]
277 }
278 }
924b3b67 279
c6a2d572 280This causes the original authinfo hash to be merged with a hash provided by
281the realm configuration under the key C<merge_hash> key. This is a deep merge
282and in the case of a conflict, the hash specified by merge_hash takes
283precedence over what was passed into the authenticate or find_user call. The
284method of merging is described in detail in the L<HASH MERGING> section below.
285
286=item new_hash
287
288 store_adaptor => {
289 method => 'new_hash',
290 new_hash => {
291 username => '+(user)', # this sets username to the value of $originalhash{user}
924b3b67 292 user_source => 'openid'
c6a2d572 293 }
294 }
924b3b67 295
296This causes the original authinfo hash to be set aside and replaced with a new hash provided under the
c6a2d572 297C<new_hash> key. The new hash can grab portions of the original hash. This can be used to remap the authinfo
298into a new format. See the L<HASH MERGING> section for information on how to do this.
299
300=item code
301
302 store_adaptor => {
303 method => 'code',
304 code => sub {
305 my ($realmname, $original_authinfo, $hashref_to_config ) = @_;
306 my $newauthinfo = {};
307 ## do something
924b3b67 308 return $newauthinfo;
c6a2d572 309 }
310 }
311
312The C<code> method allows for more complex filtering by executing code
313provided as a subroutine reference in the C<code> key. The realm name,
314original auth info and the portion of the config specific to this filter are
315passed as arguments to the provided subroutine. In the above example, it would
316be the entire store_adaptor hash. If you were using a code ref in a
317credential_adaptor, you'd get the credential_adapter config instead.
318
319=item action
320
321 credential_adaptor => {
322 method => 'action',
323 controller => 'UserProcessing',
324 action => 'FilterCredentials'
325 }
924b3b67 326
c6a2d572 327The C<action> method causes the adaptor to delegate filtering to a Catalyst
328action. This is similar to the code ref above, except that instead of simply
329calling the routine, the action specified is called via C<<$c->forward>>. The
924b3b67 330arguments passed to the action are the same as the code method as well,
c6a2d572 331namely the realm name, the original authinfo hash and the config for the adaptor.
332
333=back
334
335=head1 HASH MERGING
336
337The hash merging mechanism in Catalyst::Authentication::Realm::Adaptor is not
338a simple merge of two hashes. It has some niceties which allow for both
339re-mapping of existing keys, and a mechanism for removing keys from the
340original hash. When using the 'merge_hash' method above, the keys from the
341original hash and the keys for the merge hash are simply combined with the
342merge_hash taking precedence in the case of a key conflict. If there are
343sub-hashes they are merged as well.
344
345If both the source and merge hash contain an array for a given hash-key, the
924b3b67 346values in the merge array are appended to the original array. Note that hashes
347within arrays will not be merged, and will instead simply be copied.
c6a2d572 348
349Simple values are left intact, and in the case of a key existing in both
350hashes, the value from the merge_hash takes precedence. Note that in the case
351of a key conflict where the values are of different types, the value from the
352merge_hash will be used and no attempt is made to merge or otherwise convert
353them.
354
355=head2 Advanced merging
356
357Whether you are using C<merge_hash> or C<new_hash> as the method, you have access
924b3b67 358to the values from the original authinfo hash. In your new or merged hash, you
359can use values from anywhere within the original hash. You do this by setting
c6a2d572 360the value for the key you want to set to a special string indicating the key
924b3b67 361path in the original hash. The string is formatted as follows:
c6a2d572 362C<<'+(key1.key2.key3)'>> This will grab the hash associated with key1, retrieve the hash
363associated with key2, and finally obtain the value associated with key3. This is easier to
364show than to explain:
365
924b3b67 366 my $originalhash = {
c6a2d572 367 user => {
368 details => {
369 age => 27,
370 haircolor => 'black',
371 favoritenumbers => [ 17, 42, 19 ]
372 }
373 }
374 };
924b3b67 375
376 my $newhash = {
c6a2d572 377 # would result in a value of 'black'
924b3b67 378 haircolor => '+(user.details.haircolor)',
379
c6a2d572 380 # bestnumber would be 42.
924b3b67 381 bestnumber => '+(user.details.favoritenumbers.1)'
c6a2d572 382 }
924b3b67 383
c6a2d572 384Given the example above, the value for the userage key would be 27, (obtained
385via C<<'+(user.details.age)'>>) and the value for bestnumber would be 42. Note
386that you can traverse both hashes and arrays using this method. This can be
387quite useful when you need the values that were passed in, but you need to put
388them under different keys.
389
390When using the C<merge_hash> method, you sometimes may want to remove an item
391from the original hash. You can do this by providing a key in your merge_hash
924b3b67 392at the same point, but setting it's value to '-()'. This will remove the key
c6a2d572 393entirely from the resultant hash. This works better than simply setting the
394value to undef in some cases.
395
396=head1 NOTES and CAVEATS
397
924b3b67 398The authentication system for Catalyst is quite flexible. In most cases this
c6a2d572 399module is not needed. Evidence of this fact is that the Catalyst auth system
400was substantially unchanged for 2+ years prior to this modules first release.
401If you are looking at this module, then there is a good chance your problem would
924b3b67 402be better solved by adjusting your credential or store directly.
c6a2d572 403
924b3b67 404That said, there are some areas where this module can be particularly useful.
c6a2d572 405For example, this module allows for global application of additional arguments
406to authinfo for a certain realm via your config. It also allows for preliminary
924b3b67 407testing of alternate configs before you adjust every C<< $c->authenticate() >> call
c6a2d572 408within your application.
409
410It is also useful when combined with the various external authentication
411modules available, such as OpenID, OAuth or Facebook. These modules expect to
412store their user information in the Hash provided by the Minimal user store.
413Often, however, you want to store user information locally in a database or
414other storage mechanism. Doing this lies somewhere between difficult and
415impossible normally. With the Adapter realm, you can massage the authinfo hash
416between the credential's verification and the creation of the local user, and
417instead use the information returned to look up a user instead.
418
924b3b67 419Using the external auth mechanisms and the C<action> method, you can actually
420trigger an action to create a user record on the fly when the user has
c6a2d572 421authenticated via an external method. These are just some of the possibilities
422that Adaptor provides that would otherwise be very difficult to accomplish,
423even with Catalyst's flexible authentication system.
424
425With all of that said, caution is warranted when using this module. It modifies
426the behavior of the application in ways that are not obvious and can therefore
924b3b67 427lead to extremely hard to track-down bugs. This is especially true when using
428the C<action> filter method. When a developer calls C<< $c->authenticate() >>
429they are not expecting any actions to be called before it returns.
c6a2d572 430
431If you use the C<action> method, I strongly recommend that you use it only as a
924b3b67 432filter routine and do not do other catalyst dispatch related activities (such as
c6a2d572 433further forwards, detach's or redirects). Also note that it is B<EXTREMELY
924b3b67 434DANGEROUS> to call authentication routines from within a filter action. It is
c6a2d572 435extremely easy to accidentally create an infinite recursion bug which can crash
436your Application. In short - B<DON'T DO IT>.
437
438=head1 AUTHOR
439
440Jay Kuri, C<< <jayk at cpan.org> >>
441
442=head1 BUGS
443
444Please report any bugs or feature requests to C<bug-catalyst-authentication-realm-adaptor at rt.cpan.org>, or through
445the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Authentication-Realm-Adaptor>. I will be notified, and then you'll
446automatically be notified of progress on your bug as I make changes.
447
448
449=head1 SUPPORT
450
451You can find documentation for this module with the perldoc command.
452
453 perldoc Catalyst::Authentication::Realm::Adaptor
454
455You can also look for information at:
456
457=over 4
458
459=item * Search CPAN
460
461L<http://search.cpan.org/dist/Catalyst-Authentication-Realm-Adaptor/>
462
463=item * Catalyzed.org Wiki
464
465L<http://wiki.catalyzed.org/cpan-modules/Catalyst-Authentication-Realm-Adaptor>
466
467=back
468
469
470=head1 ACKNOWLEDGEMENTS
471
472
473=head1 COPYRIGHT & LICENSE
474
475Copyright 2009 Jay Kuri, all rights reserved.
476
477This program is free software; you can redistribute it and/or modify it
478under the same terms as Perl itself.
479
480
481=cut
482
4831; # End of Catalyst::Authentication::Realm::Adaptor